[
  {
    "path": ".cargo/config.toml",
    "content": "[build]\nrustflags = [\"-Wunused-crate-dependencies\"]"
  },
  {
    "path": ".devcontainer/dev.compose.yaml",
    "content": "services:\n  dev:\n    image: mcr.microsoft.com/devcontainers/rust:1-1-bullseye\n    volumes:\n      # Mount the root folder that contains .git\n      - ../:/workspace:cached\n      - /var/run/docker.sock:/var/run/docker.sock\n      - /proc:/proc\n      - repos:/etc/komodo/repos\n      - stacks:/etc/komodo/stacks\n    command: sleep infinity\n    ports:\n      - \"9121:9121\"\n    environment:\n      KOMODO_FIRST_SERVER: http://localhost:8120\n      KOMODO_DATABASE_ADDRESS: db\n      KOMODO_ENABLE_NEW_USERS: true\n      KOMODO_LOCAL_AUTH: true\n      KOMODO_JWT_SECRET: a_random_secret\n    links:\n      - db\n    # ...\n\n  db:\n    extends:\n      file: ../dev.compose.yaml\n      service: ferretdb\n\nvolumes:\n  data:\n  repo-cache:\n  repos:\n  stacks:"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.com/devcontainers/templates/tree/main/src/rust\n{\n\t\"name\": \"Komodo\",\n\t// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile\n\t//\"image\": \"mcr.microsoft.com/devcontainers/rust:1-1-bullseye\",\n\t\"dockerComposeFile\": [\"dev.compose.yaml\"],\n\t\"workspaceFolder\": \"/workspace\",\n  \t\"service\": \"dev\",\n\t// Features to add to the dev container. More info: https://containers.dev/features.\n\t\"features\": {\n\t\t\"ghcr.io/devcontainers/features/node:1\": {\n\t\t\t\"version\": \"20.12.2\"\n\t\t},\n\t\t\"ghcr.io/devcontainers-community/features/deno:1\": {\n\n\t\t}\n\t},\n\n\t// Use 'mounts' to make the cargo cache persistent in a Docker Volume.\n\t\"mounts\": [\n\t\t{\n\t\t\t\"source\": \"devcontainer-cargo-cache-${devcontainerId}\",\n\t\t\t\"target\": \"/usr/local/cargo\",\n\t\t\t\"type\": \"volume\"\n\t\t}\n\t],\n\n\t// Use 'forwardPorts' to make a list of ports inside the container available locally.\n\t\"forwardPorts\": [\n\t\t9121\n\t],\n\n\t// Use 'postCreateCommand' to run commands after the container is created.\n\t\"postCreateCommand\": \"./.devcontainer/postCreate.sh\",\n\n\t\"runServices\": [\n\t\t\"db\"\n\t]\n\n\t// Configure tool-specific properties.\n\t// \"customizations\": {},\n\n\t// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.\n\t// \"remoteUser\": \"root\"\n}\n"
  },
  {
    "path": ".devcontainer/postCreate.sh",
    "content": "#!/bin/sh\n\ncargo install typeshare-cli"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository\nopen_collective: komodo\n"
  },
  {
    "path": ".gitignore",
    "content": "target\nnode_modules\ndist\ndeno.lock\n.env\n.env.development\n.DS_Store\n.idea\n\n/frontend/build\n/lib/ts_client/build\n\n.dev\n"
  },
  {
    "path": ".kminclude",
    "content": ".dev"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    \"recommendations\": [\n        \"rust-lang.rust-analyzer\",\n        \"tamasfe.even-better-toml\",\n        \"vadimcn.vscode-lldb\",\n        \"denoland.vscode-deno\"\n    ]\n}"
  },
  {
    "path": ".vscode/resolver.code-snippets",
    "content": "{\n\t\"resolve\": {\n\t\t\"scope\": \"rust\",\n\t\t\"prefix\": \"resolve\",\n\t\t\"body\": [\n\t\t\t\"impl Resolve<${1}, User> for State {\",\n\t\t\t\"\\tasync fn resolve(&self, ${1} { ${0} }: ${1}, _: User) -> anyhow::Result<${2}> {\",\n\t\t\t\"\\t\\ttodo!()\",\n\t\t\t\"\\t}\",\n\t\t\t\"}\"\n\t\t]\n\t},\n\t\"static\": {\n\t\t\"scope\": \"rust\",\n\t\t\"prefix\": \"static\",\n\t\t\"body\": [\n\t\t\t\"fn ${1}() -> &'static ${2} {\",\n\t\t\t\"\\tstatic ${3}: OnceLock<${2}> = OnceLock::new();\",\n\t\t\t\"\\t${3}.get_or_init(|| {\",\n\t\t\t\"\\t\\t${0}\",\n\t\t\t\"\\t})\",\n\t\t\t\"}\"\n\t\t]\n\t}\n}"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"Run Core\",\n            \"command\": \"cargo\",\n            \"args\": [\n                \"run\",\n                \"-p\",\n                \"komodo_core\",\n                \"--release\"\n            ],\n            \"options\": {\n                \"cwd\": \"${workspaceFolder}\",\n                \"env\": {\n                    \"KOMODO_CONFIG_PATH\": \"test.core.config.toml\"\n                }\n            },\n            \"problemMatcher\": [\n                \"$rustc\"\n            ]\n        },\n        {\n            \"label\": \"Build Core\",\n            \"command\": \"cargo\",\n            \"args\": [\n                \"build\",\n                \"-p\",\n                \"komodo_core\",\n                \"--release\"\n            ],\n            \"options\": {\n                \"cwd\": \"${workspaceFolder}\",\n                \"env\": {\n                    \"KOMODO_CONFIG_PATH\": \"test.core.config.toml\"\n                }\n            },\n            \"problemMatcher\": [\n                \"$rustc\"\n            ]\n        },\n        {\n            \"label\": \"Run Periphery\",\n            \"command\": \"cargo\",\n            \"args\": [\n                \"run\",\n                \"-p\",\n                \"komodo_periphery\",\n                \"--release\"\n            ],\n            \"options\": {\n                \"cwd\": \"${workspaceFolder}\",\n                \"env\": {\n                    \"KOMODO_CONFIG_PATH\": \"test.periphery.config.toml\"\n                }\n            },\n            \"problemMatcher\": [\n                \"$rustc\"\n            ]\n        },\n        {\n            \"label\": \"Build Periphery\",\n            \"command\": \"cargo\",\n            \"args\": [\n                \"build\",\n                \"-p\",\n                \"komodo_periphery\",\n                \"--release\"\n            ],\n            \"options\": {\n                \"cwd\": \"${workspaceFolder}\",\n                \"env\": {\n                    \"KOMODO_CONFIG_PATH\": \"test.periphery.config.toml\"\n                }\n            },\n            \"problemMatcher\": [\n                \"$rustc\"\n            ]\n        },\n        {\n            \"label\": \"Run Backend\",\n            \"dependsOn\": [\n                \"Run Core\",\n                \"Run Periphery\"\n            ],\n            \"problemMatcher\": [\n                \"$rustc\"\n            ]\n        },\n        {\n            \"label\": \"Build TS Client Types\",\n            \"type\": \"process\",\n            \"command\": \"node\",\n            \"args\": [\n                \"./client/core/ts/generate_types.mjs\"\n            ],\n            \"problemMatcher\": []\n        },\n        {\n            \"label\": \"Init TS Client\",\n            \"type\": \"shell\",\n            \"command\": \"yarn && yarn build && yarn link\",\n            \"options\": {\n                \"cwd\": \"${workspaceFolder}/client/core/ts\",\n            },\n            \"problemMatcher\": []\n        },\n        {\n            \"label\": \"Init Frontend Client\",\n            \"type\": \"shell\",\n            \"command\": \"yarn link komodo_client && yarn install\",\n            \"options\": {\n                \"cwd\": \"${workspaceFolder}/frontend\",\n            },\n            \"problemMatcher\": []\n        },\n        {\n            \"label\": \"Init Frontend\",\n            \"dependsOn\": [\n                \"Build TS Client Types\",\n                \"Init TS Client\",\n                \"Init Frontend Client\"\n            ],\n            \"dependsOrder\": \"sequence\",\n            \"problemMatcher\": []\n        },\n        {\n            \"label\": \"Build Frontend\",\n            \"type\": \"shell\",\n            \"command\": \"yarn build\",\n            \"options\": {\n                \"cwd\": \"${workspaceFolder}/frontend\",\n            },\n            \"problemMatcher\": []\n        },\n        {\n            \"label\": \"Prepare Frontend For Run\",\n            \"type\": \"shell\",\n            \"command\": \"cp -r ./client/core/ts/dist/. frontend/public/client/.\",\n            \"options\": {\n                \"cwd\": \"${workspaceFolder}\",\n            },\n            \"dependsOn\": [\n                \"Build TS Client Types\",\n                \"Build Frontend\"\n            ],\n            \"dependsOrder\": \"sequence\",\n            \"problemMatcher\": []\n        },\n        {\n            \"label\": \"Run Frontend\",\n            \"type\": \"shell\",\n            \"command\": \"yarn dev\",\n            \"options\": {\n                \"cwd\": \"${workspaceFolder}/frontend\",\n            },\n            \"dependsOn\": [\"Prepare Frontend For Run\"],\n            \"problemMatcher\": []\n        },\n        {\n            \"label\": \"Init\",\n            \"dependsOn\": [\n                \"Build Backend\",\n                \"Init Frontend\"\n            ],\n            \"dependsOrder\": \"sequence\",\n            \"problemMatcher\": []\n        },\n        {\n            \"label\": \"Run Komodo\",\n            \"dependsOn\": [\n                \"Run Core\",\n                \"Run Periphery\",\n                \"Run Frontend\"\n            ],\n            \"problemMatcher\": []\n        },\n    ]\n  }"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\nmembers = [\n\t\"bin/*\",\n\t\"lib/*\",\n\t\"client/core/rs\",\n\t\"client/periphery/rs\",\n]\n\n[workspace.package]\nversion = \"1.19.5\"\nedition = \"2024\"\nauthors = [\"mbecker20 <becker.maxh@gmail.com>\"]\nlicense = \"GPL-3.0-or-later\"\nrepository = \"https://github.com/moghtech/komodo\"\nhomepage = \"https://komo.do\"\n\n[profile.release]\nstrip = \"debuginfo\"\n\n[workspace.dependencies]\n# LOCAL\nkomodo_client = { path = \"client/core/rs\" }\nperiphery_client = { path = \"client/periphery/rs\" }\nenvironment_file = { path = \"lib/environment_file\" }\nenvironment = { path = \"lib/environment\" }\ninterpolate = { path = \"lib/interpolate\" }\nformatting = { path = \"lib/formatting\" }\ndatabase = { path = \"lib/database\" }\nresponse = { path = \"lib/response\" }\ncommand = { path = \"lib/command\" }\nconfig = { path = \"lib/config\" }\nlogger = { path = \"lib/logger\" }\ncache = { path = \"lib/cache\" }\ngit = { path = \"lib/git\" }\n\n# MOGH\nrun_command = { version = \"0.0.6\", features = [\"async_tokio\"] }\nserror = { version = \"0.5.1\", default-features = false }\nslack = { version = \"0.4.0\", package = \"slack_client_rs\", default-features = false, features = [\"rustls\"] }\nderive_default_builder = \"0.1.8\"\nderive_empty_traits = \"0.1.0\"\nasync_timing_util = \"1.0.0\"\npartial_derive2 = \"0.4.3\"\nderive_variants = \"1.0.0\"\nmongo_indexed = \"2.0.2\"\nresolver_api = \"3.0.0\"\ntoml_pretty = \"1.2.0\"\nmungos = \"3.2.2\"\nsvi = \"1.2.0\"\n\n# ASYNC\nreqwest = { version = \"0.12.23\", default-features = false, features = [\"json\", \"stream\", \"rustls-tls-native-roots\"] }\ntokio = { version = \"1.47.1\", features = [\"full\"] }\ntokio-util = { version = \"0.7.16\", features = [\"io\", \"codec\"] }\ntokio-stream = { version = \"0.1.17\", features = [\"sync\"] }\npin-project-lite = \"0.2.16\"\nfutures = \"0.3.31\"\nfutures-util = \"0.3.31\"\narc-swap = \"1.7.1\"\n\n# SERVER\ntokio-tungstenite = { version = \"0.27.0\", features = [\"rustls-tls-native-roots\"] }\naxum-extra = { version = \"0.10.1\", features = [\"typed-header\"] }\ntower-http = { version = \"0.6.6\", features = [\"fs\", \"cors\"] }\naxum-server = { version = \"0.7.2\", features = [\"tls-rustls\"] }\naxum = { version = \"0.8.4\", features = [\"ws\", \"json\", \"macros\"] }\n\n# SER/DE\nipnetwork = { version = \"0.21.1\", features = [\"serde\"] }\nindexmap = { version = \"2.11.1\", features = [\"serde\"] }\nserde = { version = \"1.0.219\", features = [\"derive\"] }\nstrum = { version = \"0.27.2\", features = [\"derive\"] }\nbson = { version = \"2.15.0\" } # must keep in sync with mongodb version\nserde_yaml_ng = \"0.10.0\"\nserde_json = \"1.0.145\"\nserde_qs = \"0.15.0\"\ntoml = \"0.9.5\"\n\n# ERROR\nanyhow = \"1.0.99\"\nthiserror = \"2.0.16\"\n\n# LOGGING\nopentelemetry-otlp = { version = \"0.30.0\", features = [\"tls-roots\", \"reqwest-rustls\"] }\nopentelemetry_sdk = { version = \"0.30.0\", features = [\"rt-tokio\"] }\ntracing-subscriber = { version = \"0.3.20\", features = [\"json\"] }\nopentelemetry-semantic-conventions = \"0.30.0\"\ntracing-opentelemetry = \"0.31.0\"\nopentelemetry = \"0.30.0\"\ntracing = \"0.1.41\"\n\n# CONFIG\nclap = { version = \"4.5.47\", features = [\"derive\"] }\ndotenvy = \"0.15.7\"\nenvy = \"0.4.2\"\n\n# CRYPTO / AUTH\nuuid = { version = \"1.18.1\", features = [\"v4\", \"fast-rng\", \"serde\"] }\njsonwebtoken = { version = \"9.3.1\", default-features = false }\nopenidconnect = \"4.0.1\"\nurlencoding = \"2.1.3\"\nnom_pem = \"4.0.0\"\nbcrypt = \"0.17.1\"\nbase64 = \"0.22.1\"\nrustls = \"0.23.31\"\nhmac = \"0.12.1\"\nsha2 = \"0.10.9\"\nrand = \"0.9.2\"\nhex = \"0.4.3\"\n\n# SYSTEM\nportable-pty = \"0.9.0\"\nbollard = \"0.19.2\"\nsysinfo = \"0.37.0\"\n\n# CLOUD\naws-config = \"1.8.6\"\naws-sdk-ec2 = \"1.167.0\"\naws-credential-types = \"1.2.6\"\n\n## CRON\nenglish-to-cron = \"0.1.6\"\nchrono-tz = \"0.10.4\"\nchrono = \"0.4.42\"\ncroner = \"3.0.0\"\n\n# MISC\nasync-compression = { version = \"0.4.30\", features = [\"tokio\", \"gzip\"] }\nderive_builder = \"0.20.2\"\ncomfy-table = \"7.2.1\"\ntypeshare = \"1.0.4\"\noctorust = \"0.10.0\"\ndashmap = \"6.1.0\"\nwildcard = \"0.3.0\"\ncolored = \"3.0.0\"\nregex = \"1.11.2\"\nbytes = \"1.10.1\"\nshell-escape = \"0.1.5\""
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 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 General Public License is a free, copyleft license for\nsoftware and other kinds of works.\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,\nthe GNU General Public License is 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.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\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  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\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 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. Use with the GNU Affero General Public License.\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 Affero 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 special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe 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 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 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 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 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 General Public License for more details.\n\n    You should have received a copy of the GNU 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 the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\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 GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "bin/binaries.Dockerfile",
    "content": "## Builds the Komodo Core, Periphery, and Util binaries\n## for a specific architecture.\n\nFROM rust:1.89.0-bullseye AS builder\nRUN cargo install cargo-strip\n\nWORKDIR /builder\nCOPY Cargo.toml Cargo.lock ./\nCOPY ./lib ./lib\nCOPY ./client/core/rs ./client/core/rs\nCOPY ./client/periphery ./client/periphery\nCOPY ./bin/core ./bin/core\nCOPY ./bin/periphery ./bin/periphery\nCOPY ./bin/cli ./bin/cli\n\n# Compile bin\nRUN \\\n  cargo build -p komodo_core --release && \\\n  cargo build -p komodo_periphery --release && \\\n  cargo build -p komodo_cli --release && \\\n  cargo strip\n\n# Copy just the binaries to scratch image\nFROM scratch\n\nCOPY --from=builder /builder/target/release/core /core\nCOPY --from=builder /builder/target/release/periphery /periphery\nCOPY --from=builder /builder/target/release/km /km\n\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo Binaries\"\nLABEL org.opencontainers.image.licenses=GPL-3.0"
  },
  {
    "path": "bin/chef.binaries.Dockerfile",
    "content": "## Builds the Komodo Core, Periphery, and Util binaries\n## for a specific architecture.\n\n## Uses chef for dependency caching to help speed up back-to-back builds.\n\nFROM lukemathwalker/cargo-chef:latest-rust-1.89.0-bullseye AS chef\nWORKDIR /builder\n\n# Plan just the RECIPE to see if things have changed\nFROM chef AS planner\nCOPY . .\nRUN cargo chef prepare --recipe-path recipe.json\n\nFROM chef AS builder\nRUN cargo install cargo-strip\nCOPY --from=planner /builder/recipe.json recipe.json\n# Build JUST dependencies - cached layer\nRUN cargo chef cook --release --recipe-path recipe.json\n# NOW copy again (this time into builder) and build app\nCOPY . .\nRUN \\\n  cargo build --release --bin core && \\\n  cargo build --release --bin periphery && \\\n  cargo build --release --bin km && \\\n  cargo strip\n\n# Copy just the binaries to scratch image\nFROM scratch\n\nCOPY --from=builder /builder/target/release/core /core\nCOPY --from=builder /builder/target/release/periphery /periphery\nCOPY --from=builder /builder/target/release/km /km\n\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo Binaries\"\nLABEL org.opencontainers.image.licenses=GPL-3.0"
  },
  {
    "path": "bin/cli/Cargo.toml",
    "content": "[package]\nname = \"komodo_cli\"\ndescription = \"Command line tool for Komodo\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[[bin]]\nname = \"km\"\npath = \"src/main.rs\"\n\n[dependencies]\n# local\nenvironment_file.workspace = true\nkomodo_client.workspace = true\ndatabase.workspace = true\nconfig.workspace = true\nlogger.workspace = true\n# external\nfutures-util.workspace = true\ncomfy-table.workspace = true\nserde_json.workspace = true\nserde_qs.workspace = true\nwildcard.workspace = true\ntracing.workspace = true\ncolored.workspace = true\ndotenvy.workspace = true\nanyhow.workspace = true\nchrono.workspace = true\ntokio.workspace = true\nserde.workspace = true\nclap.workspace = true\nenvy.workspace = true"
  },
  {
    "path": "bin/cli/README.md",
    "content": "# Komodo CLI\n\nKomodo CLI is a tool to execute actions on your Komodo instance from shell scripts.\n\n## Install\n\n```sh\ncargo install komodo_cli\n```\n\nNote: On Ubuntu, also requires `apt install build-essential pkg-config libssl-dev`.\n\n## Usage\n\n### Credentials\n\nConfigure a file `~/.config/komodo/creds.toml` file with contents:\n```toml\nurl = \"https://your.komodo.address\"\nkey = \"YOUR-API-KEY\"\nsecret = \"YOUR-API-SECRET\"\n```\n\nNote. You can specify a different creds file by using `--creds ./other/path.toml`.\nYou can also bypass using any file and pass the information using `--url`, `--key`, `--secret`:\n\n```sh\nkomodo --url \"https://your.komodo.address\" --key \"YOUR-API-KEY\" --secret \"YOUR-API-SECRET\" ...\n```\n\n### Run Executions\n\n```sh\n# Triggers an example build\nkomodo execute run-build test_build\n```\n\n#### Manual\n`komodo --help`\n```md\nCommand line tool to execute Komodo actions\n\nUsage: komodo [OPTIONS] <COMMAND>\n\nCommands:\n  execute  Runs an execution\n  help     Print this message or the help of the given subcommand(s)\n\nOptions:\n      --creds <CREDS>    The path to a creds file [default: /Users/max/.config/komodo/creds.toml]\n      --url <URL>        Pass url in args instead of creds file\n      --key <KEY>        Pass api key in args instead of creds file\n      --secret <SECRET>  Pass api secret in args instead of creds file\n  -y, --yes              Always continue on user confirmation prompts\n  -h, --help             Print help (see more with '--help')\n  -V, --version          Print version\n```\n\n`komodo execute --help`\n```md\nRuns an execution\n\nUsage: komodo execute <COMMAND>\n\nCommands:\n  none                    The \"null\" execution. Does nothing\n  run-procedure           Runs the target procedure. Response: [Update]\n  run-build               Runs the target build. Response: [Update]\n  cancel-build            Cancels the target build. Only does anything if the build is `building` when called. Response: [Update]\n  deploy                  Deploys the container for the target deployment. Response: [Update]\n  start-deployment        Starts the container for the target deployment. Response: [Update]\n  restart-deployment      Restarts the container for the target deployment. Response: [Update]\n  pause-deployment        Pauses the container for the target deployment. Response: [Update]\n  unpause-deployment      Unpauses the container for the target deployment. Response: [Update]\n  stop-deployment         Stops the container for the target deployment. Response: [Update]\n  destroy-deployment      Stops and destroys the container for the target deployment. Reponse: [Update]\n  clone-repo              Clones the target repo. Response: [Update]\n  pull-repo               Pulls the target repo. Response: [Update]\n  build-repo              Builds the target repo, using the attached builder. Response: [Update]\n  cancel-repo-build       Cancels the target repo build. Only does anything if the repo build is `building` when called. Response: [Update]\n  start-container         Starts the container on the target server. Response: [Update]\n  restart-container       Restarts the container on the target server. Response: [Update]\n  pause-container         Pauses the container on the target server. Response: [Update]\n  unpause-container       Unpauses the container on the target server. Response: [Update]\n  stop-container          Stops the container on the target server. Response: [Update]\n  destroy-container       Stops and destroys the container on the target server. Reponse: [Update]\n  start-all-containers    Starts all containers on the target server. Response: [Update]\n  restart-all-containers  Restarts all containers on the target server. Response: [Update]\n  pause-all-containers    Pauses all containers on the target server. Response: [Update]\n  unpause-all-containers  Unpauses all containers on the target server. Response: [Update]\n  stop-all-containers     Stops all containers on the target server. Response: [Update]\n  prune-containers        Prunes the docker containers on the target server. Response: [Update]\n  delete-network          Delete a docker network. Response: [Update]\n  prune-networks          Prunes the docker networks on the target server. Response: [Update]\n  delete-image            Delete a docker image. Response: [Update]\n  prune-images            Prunes the docker images on the target server. Response: [Update]\n  delete-volume           Delete a docker volume. Response: [Update]\n  prune-volumes           Prunes the docker volumes on the target server. Response: [Update]\n  prune-system            Prunes the docker system on the target server, including volumes. Response: [Update]\n  run-sync                Runs the target resource sync. Response: [Update]\n  deploy-stack            Deploys the target stack. `docker compose up`. Response: [Update]\n  start-stack             Starts the target stack. `docker compose start`. Response: [Update]\n  restart-stack           Restarts the target stack. `docker compose restart`. Response: [Update]\n  pause-stack             Pauses the target stack. `docker compose pause`. Response: [Update]\n  unpause-stack           Unpauses the target stack. `docker compose unpause`. Response: [Update]\n  stop-stack              Starts the target stack. `docker compose stop`. Response: [Update]\n  destroy-stack           Destoys the target stack. `docker compose down`. Response: [Update]\n  sleep                   \n  help                    Print this message or the help of the given subcommand(s)\n\nOptions:\n  -h, --help  Print help\n```\n\n### --yes\n\nYou can use `--yes` to avoid any human prompt to continue, for use in automated environments.\n\n"
  },
  {
    "path": "bin/cli/aio.Dockerfile",
    "content": "FROM rust:1.89.0-bullseye AS builder\nRUN cargo install cargo-strip\n\nWORKDIR /builder\nCOPY Cargo.toml Cargo.lock ./\nCOPY ./lib ./lib\nCOPY ./client/core/rs ./client/core/rs\nCOPY ./client/periphery ./client/periphery\nCOPY ./bin/cli ./bin/cli\n\n# Compile bin\nRUN cargo build -p komodo_cli --release && cargo strip\n\n# Copy binaries to distroless base\nFROM gcr.io/distroless/cc\n\nCOPY --from=builder /builder/target/release/km /usr/local/bin/km\n\nENV KOMODO_CLI_CONFIG_PATHS=\"/config\"\n\nCMD [ \"km\" ]\n\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo CLI\"\nLABEL org.opencontainers.image.licenses=GPL-3.0"
  },
  {
    "path": "bin/cli/docs/copy-database.md",
    "content": "# Copy Database Utility\n\nCopy the Komodo database contents between running, mongo-compatible databases.\nCan be used to move between MongoDB / FerretDB, or upgrade from FerretDB v1 to v2.\n\n```yaml\nservices:\n\n  copy_database:\n    image: ghcr.io/moghtech/komodo-cli\n    command: km database copy -y\n    environment:\n      KOMODO_DATABASE_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@source:27017\n      KOMODO_DATABASE_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}\n      KOMODO_CLI_DATABASE_TARGET_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@target:27017\n      KOMODO_CLI_DATABASE_TARGET_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}\n\n```\n\n## FerretDB v2 Update Guide\n\nUp to Komodo 1.17.5, users who wanted to use Postgres / Sqlite were instructed to deploy FerretDB v1.\nNow that v2 is out however, v1 will go largely unsupported. Users are recommended to migrate to v2 for\nthe best performance and ongoing support / updates, however the internal data structures\nhave changed and this cannot be done in-place. \n\nAlso note that FerretDB v2 no longer supports Sqlite, and only supports \na [customized Postgres distribution](https://docs.ferretdb.io/installation/documentdb/docker/).\nNonetheless, it remains a solid option for hosts which [do not support mongo](https://github.com/moghtech/komodo/issues/59).\n\nAlso note, the same basic process outlined below can also be used to move between MongoDB and FerretDB, just replace FerretDB v2\nwith the database you wish to move to.\n\n### **Step 1**: *Add* the new database to the top of your existing Komodo compose file.\n\n**Don't forget to also add the new volumes.**\n\n```yaml\n## In Komodo compose.yaml\nservices:\n  postgres2:\n    # Recommended: Pin to a specific version\n    # https://github.com/FerretDB/documentdb/pkgs/container/postgres-documentdb\n    image: ghcr.io/ferretdb/postgres-documentdb\n    labels:\n      komodo.skip: # Prevent Komodo from stopping with StopAllContainers\n    restart: unless-stopped\n    # ports:\n    #   - 5432:5432\n    volumes:\n      - postgres-data:/var/lib/postgresql/data\n    environment:\n      POSTGRES_USER: ${KOMODO_DB_USERNAME}\n      POSTGRES_PASSWORD: ${KOMODO_DB_PASSWORD}\n      POSTGRES_DB: postgres # Do not change\n\n  ferretdb2:\n    # Recommended: Pin to a specific version\n    # https://github.com/FerretDB/FerretDB/pkgs/container/ferretdb\n    image: ghcr.io/ferretdb/ferretdb\n    labels:\n      komodo.skip: # Prevent Komodo from stopping with StopAllContainers\n    restart: unless-stopped\n    depends_on:\n      - postgres2\n    # ports:\n    #   - 27017:27017\n    volumes:\n      - ferretdb-state:/state\n    environment:\n      FERRETDB_POSTGRESQL_URL: postgres://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@postgres2:5432/postgres\n\n  ...(unchanged)\n\nvolumes:\n  ...(unchanged)\n  postgres-data:\n  ferretdb-state:\n```\n\n### **Step 2**: *Add* the database copy utility to Komodo compose file.\n\nThe SOURCE_URI points to the existing database, ie the old FerretDB v1, and it depends\non whether it was deployed using Postgres or Sqlite. The example below uses the Postgres one,\nbut if you use Sqlite it should just be something like `mongodb://ferretdb:27017`.\n\n```yaml\n## In Komodo compose.yaml\nservices:\n  ...(new database)\n\n  copy_database:\n    image: ghcr.io/moghtech/komodo-cli\n    command: km database copy -y\n    environment:\n      KOMODO_DATABASE_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@ferretdb:27017/${KOMODO_DATABASE_DB_NAME:-komodo}?authMechanism=PLAIN\n      KOMODO_DATABASE_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}\n      KOMODO_CLI_DATABASE_TARGET_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@ferretdb2:27017\n      KOMODO_CLI_DATABASE_TARGET_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}\n\n  ...(unchanged)\n```\n\n### **Step 3**: *Compose Up* the new additions\n\nRun `docker compose -p komodo --env-file compose.env -f xxxxx.compose.yaml up -d`, filling in the name of your compose.yaml.\nThis will start up both the old and new database, and copy the data to the new one.\n\nWait a few moments for the `copy_database` service to finish. When it exits,\nconfirm the logs show the data was moved successfully, and move on to the next step.\n\n### **Step 4**: Point Komodo Core to the new database\n\nIn your Komodo compose.yaml, first *comment out* the `copy_database` service and old ferretdb v1 service/s.\nThen update the `core` service environment to point to `ferretdb2`.\n\n```yaml\nservices:\n  ...\n\n  core:\n    ...(unchanged)\n    environment:\n      KOMODO_DATABASE_ADDRESS: ferretdb2:27017\n      KOMODO_DATABASE_USERNAME: ${KOMODO_DB_USERNAME}\n      KOMODO_DATABASE_PASSWORD: ${KOMODO_DB_PASSWORD}\n```\n\n### **Step 5**: Final *Compose Up*\n\nRepeat the same `docker compose` command as before to apply the changes, and then try navigating to your Komodo web page.\nIf it works, congrats, **you are done**. You can clean up the compose file if you would like, removing the old volumes etc.\n\nIf it does not work, check the logs for any obvious issues, and if necessary you can undo the previous steps\nto go back to using the previous database.\n"
  },
  {
    "path": "bin/cli/multi-arch.Dockerfile",
    "content": "## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).\n## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.\n\nARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest\nARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64\nARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64\n\n# This is required to work with COPY --from\nFROM ${X86_64_BINARIES} AS x86_64\nFROM ${AARCH64_BINARIES} AS aarch64\n\nFROM debian:bullseye-slim\n\nWORKDIR /app\n\n## Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.\nCOPY --from=x86_64 /km /app/arch/linux/amd64\nCOPY --from=aarch64 /km /app/arch/linux/arm64\n\nARG TARGETPLATFORM\nRUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/km && rm -r /app/arch\n\nENV KOMODO_CLI_CONFIG_PATHS=\"/config\"\n\nCMD [ \"km\" ]\n\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo CLI\"\nLABEL org.opencontainers.image.licenses=GPL-3.0"
  },
  {
    "path": "bin/cli/runfile.toml",
    "content": "[install-cli]\nalias = \"ic\"\ndescription = \"installs the komodo-cli, available on the command line as 'km'\"\ncmd = \"cargo install --path .\""
  },
  {
    "path": "bin/cli/single-arch.Dockerfile",
    "content": "## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).\n\nARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest\n\n# This is required to work with COPY --from\nFROM ${BINARIES_IMAGE} AS binaries\n\nFROM gcr.io/distroless/cc\n\nCOPY --from=binaries /km /usr/local/bin/km\n\nENV KOMODO_CLI_CONFIG_PATHS=\"/config\"\n\nCMD [ \"km\" ]\n\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo CLI\"\nLABEL org.opencontainers.image.licenses=GPL-3.0\n"
  },
  {
    "path": "bin/cli/src/command/container.rs",
    "content": "use std::collections::{HashMap, HashSet};\n\nuse anyhow::Context;\nuse colored::Colorize;\nuse comfy_table::{Attribute, Cell, Color};\nuse futures_util::{\n  FutureExt, TryStreamExt, stream::FuturesUnordered,\n};\nuse komodo_client::{\n  api::read::{\n    InspectDockerContainer, ListAllDockerContainers, ListServers,\n  },\n  entities::{\n    config::cli::args::container::{\n      Container, ContainerCommand, InspectContainer,\n    },\n    docker::{\n      self,\n      container::{ContainerListItem, ContainerStateStatusEnum},\n    },\n  },\n};\n\nuse crate::{\n  command::{\n    PrintTable, clamp_sha, matches_wildcards, parse_wildcards,\n    print_items,\n  },\n  config::cli_config,\n};\n\npub async fn handle(container: &Container) -> anyhow::Result<()> {\n  match &container.command {\n    None => list_containers(container).await,\n    Some(ContainerCommand::Inspect(inspect)) => {\n      inspect_container(inspect).await\n    }\n  }\n}\n\nasync fn list_containers(\n  Container {\n    all,\n    down,\n    links,\n    reverse,\n    containers: names,\n    images,\n    networks,\n    servers,\n    format,\n    command: _,\n  }: &Container,\n) -> anyhow::Result<()> {\n  let client = super::komodo_client().await?;\n  let (server_map, containers) = tokio::try_join!(\n    client\n      .read(ListServers::default())\n      .map(|res| res.map(|res| res\n        .into_iter()\n        .map(|s| (s.id.clone(), s))\n        .collect::<HashMap<_, _>>())),\n    client.read(ListAllDockerContainers {\n      servers: Default::default()\n    }),\n  )?;\n\n  // (Option<Server Name>, Container)\n  let containers = containers.into_iter().map(|c| {\n    let server = if let Some(server_id) = c.server_id.as_ref()\n      && let Some(server) = server_map.get(server_id)\n    {\n      server\n    } else {\n      return (None, c);\n    };\n    (Some(server.name.as_str()), c)\n  });\n\n  let names = parse_wildcards(names);\n  let servers = parse_wildcards(servers);\n  let images = parse_wildcards(images);\n  let networks = parse_wildcards(networks);\n\n  let mut containers = containers\n    .into_iter()\n    .filter(|(server_name, c)| {\n      let state_check = if *all {\n        true\n      } else if *down {\n        !matches!(c.state, ContainerStateStatusEnum::Running)\n      } else {\n        matches!(c.state, ContainerStateStatusEnum::Running)\n      };\n      let network_check = matches_wildcards(\n        &networks,\n        &c.network_mode\n          .as_deref()\n          .map(|n| vec![n])\n          .unwrap_or_default(),\n      ) || matches_wildcards(\n        &networks,\n        &c.networks.iter().map(String::as_str).collect::<Vec<_>>(),\n      );\n      state_check\n        && network_check\n        && matches_wildcards(&names, &[c.name.as_str()])\n        && matches_wildcards(\n          &servers,\n          &server_name\n            .as_deref()\n            .map(|i| vec![i])\n            .unwrap_or_default(),\n        )\n        && matches_wildcards(\n          &images,\n          &c.image.as_deref().map(|i| vec![i]).unwrap_or_default(),\n        )\n    })\n    .collect::<Vec<_>>();\n  containers.sort_by(|(a_s, a), (b_s, b)| {\n    a.state\n      .cmp(&b.state)\n      .then(a.name.cmp(&b.name))\n      .then(a_s.cmp(b_s))\n      .then(a.network_mode.cmp(&b.network_mode))\n      .then(a.image.cmp(&b.image))\n  });\n  if *reverse {\n    containers.reverse();\n  }\n  print_items(containers, *format, *links)?;\n  Ok(())\n}\n\npub async fn inspect_container(\n  inspect: &InspectContainer,\n) -> anyhow::Result<()> {\n  let client = super::komodo_client().await?;\n  let (server_map, mut containers) = tokio::try_join!(\n    client\n      .read(ListServers::default())\n      .map(|res| res.map(|res| res\n        .into_iter()\n        .map(|s| (s.id.clone(), s))\n        .collect::<HashMap<_, _>>())),\n    client.read(ListAllDockerContainers {\n      servers: Default::default()\n    }),\n  )?;\n\n  containers.iter_mut().for_each(|c| {\n    let Some(server_id) = c.server_id.as_ref() else {\n      return;\n    };\n    let Some(server) = server_map.get(server_id) else {\n      c.server_id = Some(String::from(\"Unknown\"));\n      return;\n    };\n    c.server_id = Some(server.name.clone());\n  });\n\n  let names = [inspect.container.to_string()];\n  let names = parse_wildcards(&names);\n  let servers = parse_wildcards(&inspect.servers);\n\n  let mut containers = containers\n    .into_iter()\n    .filter(|c| {\n      matches_wildcards(&names, &[c.name.as_str()])\n        && matches_wildcards(\n          &servers,\n          &c.server_id\n            .as_deref()\n            .map(|i| vec![i])\n            .unwrap_or_default(),\n        )\n    })\n    .map(|c| async move {\n      client\n        .read(InspectDockerContainer {\n          container: c.name,\n          server: c.server_id.context(\"No server...\")?,\n        })\n        .await\n    })\n    .collect::<FuturesUnordered<_>>()\n    .try_collect::<Vec<_>>()\n    .await?;\n\n  containers.sort_by(|a, b| a.name.cmp(&b.name));\n\n  match containers.len() {\n    0 => {\n      println!(\n        \"{}: Did not find any containers matching '{}'\",\n        \"INFO\".green(),\n        inspect.container.bold()\n      );\n    }\n    1 => {\n      println!(\"{}\", serialize_container(inspect, &containers[0])?);\n    }\n    _ => {\n      let containers = containers\n        .iter()\n        .map(|c| serialize_container(inspect, c))\n        .collect::<anyhow::Result<Vec<_>>>()?\n        .join(\"\\n\");\n      println!(\"{containers}\");\n    }\n  }\n\n  Ok(())\n}\n\nfn serialize_container(\n  inspect: &InspectContainer,\n  container: &docker::container::Container,\n) -> anyhow::Result<String> {\n  let res = if inspect.state {\n    serde_json::to_string_pretty(&container.state)\n  } else if inspect.mounts {\n    serde_json::to_string_pretty(&container.mounts)\n  } else if inspect.host_config {\n    serde_json::to_string_pretty(&container.host_config)\n  } else if inspect.config {\n    serde_json::to_string_pretty(&container.config)\n  } else if inspect.network_settings {\n    serde_json::to_string_pretty(&container.network_settings)\n  } else {\n    serde_json::to_string_pretty(container)\n  }\n  .context(\"Failed to serialize items to JSON\")?;\n  Ok(res)\n}\n\n// (Option<Server Name>, Container)\nimpl PrintTable for (Option<&'_ str>, ContainerListItem) {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\n        \"Container\",\n        \"State\",\n        \"Server\",\n        \"Ports\",\n        \"Networks\",\n        \"Image\",\n        \"Link\",\n      ]\n    } else {\n      &[\"Container\", \"State\", \"Server\", \"Ports\", \"Networks\", \"Image\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<Cell> {\n    let color = match self.1.state {\n      ContainerStateStatusEnum::Running => Color::Green,\n      ContainerStateStatusEnum::Paused => Color::DarkYellow,\n      ContainerStateStatusEnum::Empty => Color::Grey,\n      _ => Color::Red,\n    };\n    let mut networks = HashSet::new();\n    if let Some(network) = self.1.network_mode {\n      networks.insert(network);\n    }\n    for network in self.1.networks {\n      networks.insert(network);\n    }\n    let mut networks = networks.into_iter().collect::<Vec<_>>();\n    networks.sort();\n    let mut ports = self\n      .1\n      .ports\n      .into_iter()\n      .flat_map(|p| p.public_port.map(|p| p.to_string()))\n      .collect::<HashSet<_>>()\n      .into_iter()\n      .collect::<Vec<_>>();\n    ports.sort();\n    let ports = if ports.is_empty() {\n      Cell::new(\"\")\n    } else {\n      Cell::new(format!(\":{}\", ports.join(\", :\")))\n    };\n\n    let image = self.1.image.as_deref().unwrap_or(\"Unknown\");\n    let mut res = vec![\n      Cell::new(self.1.name.clone()).add_attribute(Attribute::Bold),\n      Cell::new(self.1.state.to_string())\n        .fg(color)\n        .add_attribute(Attribute::Bold),\n      Cell::new(self.0.unwrap_or(\"Unknown\")),\n      ports,\n      Cell::new(networks.join(\", \")),\n      Cell::new(clamp_sha(image)),\n    ];\n    if !links {\n      return res;\n    }\n    let link = if let Some(server_id) = self.1.server_id {\n      format!(\n        \"{}/servers/{server_id}/container/{}\",\n        cli_config().host,\n        self.1.name\n      )\n    } else {\n      String::new()\n    };\n    res.push(Cell::new(link));\n    res\n  }\n}\n"
  },
  {
    "path": "bin/cli/src/command/database.rs",
    "content": "use std::path::Path;\n\nuse anyhow::Context;\nuse colored::Colorize;\nuse komodo_client::entities::{\n  config::cli::args::database::DatabaseCommand, optional_string,\n};\n\nuse crate::{command::sanitize_uri, config::cli_config};\n\npub async fn handle(command: &DatabaseCommand) -> anyhow::Result<()> {\n  match command {\n    DatabaseCommand::Backup { yes, .. } => backup(*yes).await,\n    DatabaseCommand::Restore {\n      restore_folder,\n      index,\n      yes,\n      ..\n    } => restore(restore_folder.as_deref(), *index, *yes).await,\n    DatabaseCommand::Prune { yes, .. } => prune(*yes).await,\n    DatabaseCommand::Copy { yes, index, .. } => {\n      copy(*index, *yes).await\n    }\n  }\n}\n\nasync fn backup(yes: bool) -> anyhow::Result<()> {\n  let config = cli_config();\n\n  println!(\n    \"\\n🦎  {} Database {} Utility  🦎\",\n    \"Komodo\".bold(),\n    \"Backup\".green().bold()\n  );\n  println!(\n    \"\\n{}\\n\",\n    \" - Backup all database contents to gzip compressed files.\"\n      .dimmed()\n  );\n  if let Some(uri) = optional_string(&config.database.uri) {\n    println!(\"{}: {}\", \" - Source URI\".dimmed(), sanitize_uri(&uri));\n  }\n  if let Some(address) = optional_string(&config.database.address) {\n    println!(\"{}: {address}\", \" - Source Address\".dimmed());\n  }\n  if let Some(username) = optional_string(&config.database.username) {\n    println!(\"{}: {username}\", \" - Source Username\".dimmed());\n  }\n  println!(\n    \"{}: {}\\n\",\n    \" - Source Db Name\".dimmed(),\n    config.database.db_name,\n  );\n  println!(\n    \"{}: {:?}\",\n    \" - Backups Folder\".dimmed(),\n    config.backups_folder\n  );\n  if config.max_backups == 0 {\n    println!(\n      \"{}{}\",\n      \" - Backup pruning\".dimmed(),\n      \"disabled\".red().dimmed()\n    );\n  } else {\n    println!(\"{}: {}\", \" - Max Backups\".dimmed(), config.max_backups);\n  }\n\n  crate::command::wait_for_enter(\"start backup\", yes)?;\n\n  let db = database::init(&config.database).await?;\n\n  database::utils::backup(&db, &config.backups_folder).await?;\n\n  // Early return if backup pruning disabled\n  if config.max_backups == 0 {\n    return Ok(());\n  }\n\n  // Know that new backup was taken successfully at this point,\n  // safe to prune old backup folders\n\n  prune_inner().await\n}\n\nasync fn restore(\n  restore_folder: Option<&Path>,\n  index: bool,\n  yes: bool,\n) -> anyhow::Result<()> {\n  let config = cli_config();\n\n  println!(\n    \"\\n🦎  {} Database {} Utility  🦎\",\n    \"Komodo\".bold(),\n    \"Restore\".purple().bold()\n  );\n  println!(\n    \"\\n{}\\n\",\n    \" - Restores database contents from gzip compressed files.\"\n      .dimmed()\n  );\n  if let Some(uri) = optional_string(&config.database_target.uri) {\n    println!(\"{}: {}\", \" - Target URI\".dimmed(), sanitize_uri(&uri));\n  }\n  if let Some(address) =\n    optional_string(&config.database_target.address)\n  {\n    println!(\"{}: {address}\", \" - Target Address\".dimmed());\n  }\n  if let Some(username) =\n    optional_string(&config.database_target.username)\n  {\n    println!(\"{}: {username}\", \" - Target Username\".dimmed());\n  }\n  println!(\n    \"{}: {}\",\n    \" - Target Db Name\".dimmed(),\n    config.database_target.db_name,\n  );\n  if !index {\n    println!(\n      \"{}: {}\",\n      \" - Target Db Indexing\".dimmed(),\n      \"DISABLED\".red(),\n    );\n  }\n  println!(\n    \"\\n{}: {:?}\",\n    \" - Backups Folder\".dimmed(),\n    config.backups_folder\n  );\n  if let Some(restore_folder) = restore_folder {\n    println!(\"{}: {restore_folder:?}\", \" - Restore Folder\".dimmed());\n  }\n\n  crate::command::wait_for_enter(\"start restore\", yes)?;\n\n  let db = if index {\n    database::Client::new(&config.database_target).await?.db\n  } else {\n    database::init(&config.database_target).await?\n  };\n\n  database::utils::restore(\n    &db,\n    &config.backups_folder,\n    restore_folder,\n  )\n  .await\n}\n\nasync fn prune(yes: bool) -> anyhow::Result<()> {\n  let config = cli_config();\n\n  println!(\n    \"\\n🦎  {} Database {} Utility  🦎\",\n    \"Komodo\".bold(),\n    \"Backup Prune\".cyan().bold()\n  );\n  println!(\n    \"\\n{}\\n\",\n    \" - Prunes database backup folders when greater than the configured amount.\"\n      .dimmed()\n  );\n  println!(\n    \"{}: {:?}\",\n    \" - Backups Folder\".dimmed(),\n    config.backups_folder\n  );\n  if config.max_backups == 0 {\n    println!(\n      \"{}{}\",\n      \" - Backup pruning\".dimmed(),\n      \"disabled\".red().dimmed()\n    );\n  } else {\n    println!(\"{}: {}\", \" - Max Backups\".dimmed(), config.max_backups);\n  }\n\n  // Early return if backup pruning disabled\n  if config.max_backups == 0 {\n    info!(\n      \"Backup pruning is disabled, enabled using 'max_backups' (KOMODO_CLI_MAX_BACKUPS)\"\n    );\n    return Ok(());\n  }\n\n  crate::command::wait_for_enter(\"start backup prune\", yes)?;\n\n  prune_inner().await\n}\n\nasync fn prune_inner() -> anyhow::Result<()> {\n  let config = cli_config();\n\n  let mut backups_dir =\n    match tokio::fs::read_dir(&config.backups_folder)\n      .await\n      .context(\"Failed to read backups folder for prune\")\n    {\n      Ok(backups_dir) => backups_dir,\n      Err(e) => {\n        warn!(\"{e:#}\");\n        return Ok(());\n      }\n    };\n\n  let mut backup_folders = Vec::new();\n  loop {\n    match backups_dir.next_entry().await {\n      Ok(Some(entry)) => {\n        let Ok(metadata) = entry.metadata().await else {\n          continue;\n        };\n        if metadata.is_dir() {\n          backup_folders.push(entry.path());\n        }\n      }\n      Ok(None) => break,\n      Err(_) => {\n        continue;\n      }\n    }\n  }\n  // Ordered from oldest -> newest\n  backup_folders.sort();\n\n  let max_backups = config.max_backups as usize;\n  let backup_folders_len = backup_folders.len();\n\n  // Early return if under the backup count threshold\n  if backup_folders_len <= max_backups {\n    info!(\"No backups to prune\");\n    return Ok(());\n  }\n\n  let to_delete =\n    &backup_folders[..(backup_folders_len - max_backups)];\n\n  info!(\"Pruning old backups: {to_delete:?}\");\n\n  for path in to_delete {\n    if let Err(e) =\n      tokio::fs::remove_dir_all(path).await.with_context(|| {\n        format!(\"Failed to delete backup folder at {path:?}\")\n      })\n    {\n      warn!(\"{e:#}\");\n    }\n  }\n\n  Ok(())\n}\n\nasync fn copy(index: bool, yes: bool) -> anyhow::Result<()> {\n  let config = cli_config();\n\n  println!(\n    \"\\n🦎  {} Database {} Utility  🦎\",\n    \"Komodo\".bold(),\n    \"Copy\".blue().bold()\n  );\n  println!(\n    \"\\n{}\\n\",\n    \" - Copies database contents to another database.\".dimmed()\n  );\n\n  if let Some(uri) = optional_string(&config.database.uri) {\n    println!(\"{}: {}\", \" - Source URI\".dimmed(), sanitize_uri(&uri));\n  }\n  if let Some(address) = optional_string(&config.database.address) {\n    println!(\"{}: {address}\", \" - Source Address\".dimmed());\n  }\n  if let Some(username) = optional_string(&config.database.username) {\n    println!(\"{}: {username}\", \" - Source Username\".dimmed());\n  }\n  println!(\n    \"{}: {}\\n\",\n    \" - Source Db Name\".dimmed(),\n    config.database.db_name,\n  );\n\n  if let Some(uri) = optional_string(&config.database_target.uri) {\n    println!(\"{}: {}\", \" - Target URI\".dimmed(), sanitize_uri(&uri));\n  }\n  if let Some(address) =\n    optional_string(&config.database_target.address)\n  {\n    println!(\"{}: {address}\", \" - Target Address\".dimmed());\n  }\n  if let Some(username) =\n    optional_string(&config.database_target.username)\n  {\n    println!(\"{}: {username}\", \" - Target Username\".dimmed());\n  }\n  println!(\n    \"{}: {}\",\n    \" - Target Db Name\".dimmed(),\n    config.database_target.db_name,\n  );\n  if !index {\n    println!(\n      \"{}: {}\",\n      \" - Target Db Indexing\".dimmed(),\n      \"DISABLED\".red(),\n    );\n  }\n\n  crate::command::wait_for_enter(\"start copy\", yes)?;\n\n  let source_db = database::init(&config.database).await?;\n  let target_db = if index {\n    database::Client::new(&config.database_target).await?.db\n  } else {\n    database::init(&config.database_target).await?\n  };\n\n  database::utils::copy(&source_db, &target_db).await\n}\n"
  },
  {
    "path": "bin/cli/src/command/execute.rs",
    "content": "use std::time::Duration;\n\nuse colored::Colorize;\nuse futures_util::{StreamExt, stream::FuturesUnordered};\nuse komodo_client::{\n  api::execute::{\n    BatchExecutionResponse, BatchExecutionResponseItem, Execution,\n  },\n  entities::{resource_link, update::Update},\n};\n\nuse crate::config::cli_config;\n\nenum ExecutionResult {\n  Single(Box<Update>),\n  Batch(BatchExecutionResponse),\n}\n\npub async fn handle(\n  execution: &Execution,\n  yes: bool,\n) -> anyhow::Result<()> {\n  if matches!(execution, Execution::None(_)) {\n    println!(\"Got 'none' execution. Doing nothing...\");\n    tokio::time::sleep(Duration::from_secs(3)).await;\n    println!(\"Finished doing nothing. Exiting...\");\n    std::process::exit(0);\n  }\n\n  println!(\"\\n{}: Execution\", \"Mode\".dimmed());\n  match execution {\n    Execution::None(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::RunAction(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchRunAction(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::RunProcedure(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchRunProcedure(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::RunBuild(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchRunBuild(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::CancelBuild(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::Deploy(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchDeploy(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PullDeployment(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::StartDeployment(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::RestartDeployment(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PauseDeployment(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::UnpauseDeployment(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::StopDeployment(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::DestroyDeployment(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchDestroyDeployment(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::CloneRepo(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchCloneRepo(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PullRepo(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchPullRepo(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BuildRepo(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchBuildRepo(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::CancelRepoBuild(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::StartContainer(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::RestartContainer(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PauseContainer(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::UnpauseContainer(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::StopContainer(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::DestroyContainer(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::StartAllContainers(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::RestartAllContainers(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PauseAllContainers(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::UnpauseAllContainers(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::StopAllContainers(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PruneContainers(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::DeleteNetwork(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PruneNetworks(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::DeleteImage(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PruneImages(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::DeleteVolume(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PruneVolumes(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PruneDockerBuilders(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PruneBuildx(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PruneSystem(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::RunSync(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::CommitSync(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::DeployStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchDeployStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::DeployStackIfChanged(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchDeployStackIfChanged(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PullStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchPullStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::StartStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::RestartStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::PauseStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::UnpauseStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::StopStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::DestroyStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BatchDestroyStack(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::RunStackService(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::TestAlerter(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::SendAlert(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::ClearRepoCache(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::BackupCoreDatabase(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::GlobalAutoUpdate(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n    Execution::Sleep(data) => {\n      println!(\"{}: {data:?}\", \"Data\".dimmed())\n    }\n  }\n\n  super::wait_for_enter(\"run execution\", yes)?;\n\n  info!(\"Running Execution...\");\n\n  let client = super::komodo_client().await?;\n\n  let res = match execution.clone() {\n    Execution::RunAction(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchRunAction(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::RunProcedure(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchRunProcedure(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::RunBuild(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchRunBuild(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::CancelBuild(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::Deploy(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchDeploy(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::PullDeployment(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::StartDeployment(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::RestartDeployment(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PauseDeployment(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::UnpauseDeployment(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::StopDeployment(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::DestroyDeployment(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchDestroyDeployment(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::CloneRepo(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchCloneRepo(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::PullRepo(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchPullRepo(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::BuildRepo(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchBuildRepo(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::CancelRepoBuild(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::StartContainer(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::RestartContainer(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PauseContainer(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::UnpauseContainer(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::StopContainer(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::DestroyContainer(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::StartAllContainers(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::RestartAllContainers(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PauseAllContainers(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::UnpauseAllContainers(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::StopAllContainers(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PruneContainers(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::DeleteNetwork(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PruneNetworks(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::DeleteImage(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PruneImages(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::DeleteVolume(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PruneVolumes(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PruneDockerBuilders(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PruneBuildx(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PruneSystem(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::RunSync(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::CommitSync(request) => client\n      .write(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::DeployStack(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchDeployStack(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::DeployStackIfChanged(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchDeployStackIfChanged(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::PullStack(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchPullStack(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::StartStack(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::RestartStack(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::PauseStack(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::UnpauseStack(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::StopStack(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::DestroyStack(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BatchDestroyStack(request) => {\n      client.execute(request).await.map(ExecutionResult::Batch)\n    }\n    Execution::RunStackService(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::TestAlerter(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::SendAlert(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::ClearRepoCache(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::BackupCoreDatabase(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::GlobalAutoUpdate(request) => client\n      .execute(request)\n      .await\n      .map(|u| ExecutionResult::Single(u.into())),\n    Execution::Sleep(request) => {\n      let duration =\n        Duration::from_millis(request.duration_ms as u64);\n      tokio::time::sleep(duration).await;\n      println!(\"Finished sleeping!\");\n      std::process::exit(0)\n    }\n    Execution::None(_) => unreachable!(),\n  };\n\n  match res {\n    Ok(ExecutionResult::Single(update)) => {\n      poll_update_until_complete(&update).await\n    }\n    Ok(ExecutionResult::Batch(updates)) => {\n      let mut handles = updates\n        .iter()\n        .map(|update| async move {\n          match update {\n            BatchExecutionResponseItem::Ok(update) => {\n              poll_update_until_complete(update).await\n            }\n            BatchExecutionResponseItem::Err(e) => {\n              error!(\"{e:#?}\");\n              Ok(())\n            }\n          }\n        })\n        .collect::<FuturesUnordered<_>>();\n      while let Some(res) = handles.next().await {\n        match res {\n          Ok(()) => {}\n          Err(e) => {\n            error!(\"{e:#?}\");\n          }\n        }\n      }\n      Ok(())\n    }\n    Err(e) => {\n      error!(\"{e:#?}\");\n      Ok(())\n    }\n  }\n}\n\nasync fn poll_update_until_complete(\n  update: &Update,\n) -> anyhow::Result<()> {\n  let link = if update.id.is_empty() {\n    let (resource_type, id) = update.target.extract_variant_id();\n    resource_link(&cli_config().host, resource_type, id)\n  } else {\n    format!(\"{}/updates/{}\", cli_config().host, update.id)\n  };\n  println!(\"Link: '{}'\", link.bold());\n\n  let client = super::komodo_client().await?;\n\n  let timer = tokio::time::Instant::now();\n  let update = client.poll_update_until_complete(&update.id).await?;\n  if update.success {\n    println!(\n      \"FINISHED in {}: {}\",\n      format!(\"{:.1?}\", timer.elapsed()).bold(),\n      \"EXECUTION SUCCESSFUL\".green(),\n    );\n  } else {\n    eprintln!(\n      \"FINISHED in {}: {}\",\n      format!(\"{:.1?}\", timer.elapsed()).bold(),\n      \"EXECUTION FAILED\".red(),\n    );\n  }\n  Ok(())\n}\n"
  },
  {
    "path": "bin/cli/src/command/list.rs",
    "content": "use std::{cmp::Ordering, collections::HashMap};\n\nuse comfy_table::{Attribute, Cell, Color};\nuse futures_util::{FutureExt, try_join};\nuse komodo_client::{\n  KomodoClient,\n  api::read::{\n    ListActions, ListAlerters, ListBuilders, ListBuilds,\n    ListDeployments, ListProcedures, ListRepos, ListResourceSyncs,\n    ListSchedules, ListServers, ListStacks, ListTags,\n  },\n  entities::{\n    ResourceTargetVariant,\n    action::{ActionListItem, ActionListItemInfo, ActionState},\n    alerter::{AlerterListItem, AlerterListItemInfo},\n    build::{BuildListItem, BuildListItemInfo, BuildState},\n    builder::{BuilderListItem, BuilderListItemInfo},\n    config::cli::args::{\n      self,\n      list::{ListCommand, ResourceFilters},\n    },\n    deployment::{\n      DeploymentListItem, DeploymentListItemInfo, DeploymentState,\n    },\n    procedure::{\n      ProcedureListItem, ProcedureListItemInfo, ProcedureState,\n    },\n    repo::{RepoListItem, RepoListItemInfo, RepoState},\n    resource::{ResourceListItem, ResourceQuery},\n    resource_link,\n    schedule::Schedule,\n    server::{ServerListItem, ServerListItemInfo, ServerState},\n    stack::{StackListItem, StackListItemInfo, StackState},\n    sync::{\n      ResourceSyncListItem, ResourceSyncListItemInfo,\n      ResourceSyncState,\n    },\n  },\n};\nuse serde::Serialize;\n\nuse crate::{\n  command::{\n    PrintTable, format_timetamp, matches_wildcards, parse_wildcards,\n    print_items,\n  },\n  config::cli_config,\n};\n\npub async fn handle(list: &args::list::List) -> anyhow::Result<()> {\n  match &list.command {\n    None => list_all(list).await,\n    Some(ListCommand::Servers(filters)) => {\n      list_resources::<ServerListItem>(filters, false).await\n    }\n    Some(ListCommand::Stacks(filters)) => {\n      list_resources::<StackListItem>(filters, false).await\n    }\n    Some(ListCommand::Deployments(filters)) => {\n      list_resources::<DeploymentListItem>(filters, false).await\n    }\n    Some(ListCommand::Builds(filters)) => {\n      list_resources::<BuildListItem>(filters, false).await\n    }\n    Some(ListCommand::Repos(filters)) => {\n      list_resources::<RepoListItem>(filters, false).await\n    }\n    Some(ListCommand::Procedures(filters)) => {\n      list_resources::<ProcedureListItem>(filters, false).await\n    }\n    Some(ListCommand::Actions(filters)) => {\n      list_resources::<ActionListItem>(filters, false).await\n    }\n    Some(ListCommand::Syncs(filters)) => {\n      list_resources::<ResourceSyncListItem>(filters, false).await\n    }\n    Some(ListCommand::Builders(filters)) => {\n      list_resources::<BuilderListItem>(filters, false).await\n    }\n    Some(ListCommand::Alerters(filters)) => {\n      list_resources::<AlerterListItem>(filters, false).await\n    }\n    Some(ListCommand::Schedules(filters)) => {\n      list_schedules(filters).await\n    }\n  }\n}\n\n/// Includes all resources besides builds and alerters.\nasync fn list_all(list: &args::list::List) -> anyhow::Result<()> {\n  let filters: ResourceFilters = list.clone().into();\n  let client = super::komodo_client().await?;\n  let (\n    tags,\n    mut servers,\n    mut stacks,\n    mut deployments,\n    mut builds,\n    mut repos,\n    mut procedures,\n    mut actions,\n    mut syncs,\n  ) = try_join!(\n    client.read(ListTags::default()).map(|res| res.map(|res| res\n      .into_iter()\n      .map(|t| (t.id, t.name))\n      .collect::<HashMap<_, _>>())),\n    ServerListItem::list(client, &filters, true),\n    StackListItem::list(client, &filters, true),\n    DeploymentListItem::list(client, &filters, true),\n    BuildListItem::list(client, &filters, true),\n    RepoListItem::list(client, &filters, true),\n    ProcedureListItem::list(client, &filters, true),\n    ActionListItem::list(client, &filters, true),\n    ResourceSyncListItem::list(client, &filters, true),\n  )?;\n\n  if !servers.is_empty() {\n    fix_tags(&mut servers, &tags);\n    print_items(servers, filters.format, list.links)?;\n    println!();\n  }\n\n  if !stacks.is_empty() {\n    fix_tags(&mut stacks, &tags);\n    print_items(stacks, filters.format, list.links)?;\n    println!();\n  }\n\n  if !deployments.is_empty() {\n    fix_tags(&mut deployments, &tags);\n    print_items(deployments, filters.format, list.links)?;\n    println!();\n  }\n\n  if !builds.is_empty() {\n    fix_tags(&mut builds, &tags);\n    print_items(builds, filters.format, list.links)?;\n    println!();\n  }\n\n  if !repos.is_empty() {\n    fix_tags(&mut repos, &tags);\n    print_items(repos, filters.format, list.links)?;\n    println!();\n  }\n\n  if !procedures.is_empty() {\n    fix_tags(&mut procedures, &tags);\n    print_items(procedures, filters.format, list.links)?;\n    println!();\n  }\n\n  if !actions.is_empty() {\n    fix_tags(&mut actions, &tags);\n    print_items(actions, filters.format, list.links)?;\n    println!();\n  }\n\n  if !syncs.is_empty() {\n    fix_tags(&mut syncs, &tags);\n    print_items(syncs, filters.format, list.links)?;\n    println!();\n  }\n\n  Ok(())\n}\n\nasync fn list_resources<T>(\n  filters: &ResourceFilters,\n  minimal: bool,\n) -> anyhow::Result<()>\nwhere\n  T: ListResources,\n  ResourceListItem<T::Info>: PrintTable + Serialize,\n{\n  let client = crate::command::komodo_client().await?;\n  let (mut resources, tags) = tokio::try_join!(\n    T::list(client, filters, minimal),\n    client.read(ListTags::default()).map(|res| res.map(|res| res\n      .into_iter()\n      .map(|t| (t.id, t.name))\n      .collect::<HashMap<_, _>>()))\n  )?;\n  fix_tags(&mut resources, &tags);\n  if !resources.is_empty() {\n    print_items(resources, filters.format, filters.links)?;\n  }\n  Ok(())\n}\n\nasync fn list_schedules(\n  filters: &ResourceFilters,\n) -> anyhow::Result<()> {\n  let client = crate::command::komodo_client().await?;\n  let (mut schedules, tags) = tokio::try_join!(\n    client\n      .read(ListSchedules {\n        tags: filters.tags.clone(),\n        tag_behavior: Default::default(),\n      })\n      .map(|res| res.map(|res| res\n        .into_iter()\n        .filter(|s| s.next_scheduled_run.is_some())\n        .collect::<Vec<_>>())),\n    client.read(ListTags::default()).map(|res| res.map(|res| res\n      .into_iter()\n      .map(|t| (t.id, t.name))\n      .collect::<HashMap<_, _>>()))\n  )?;\n  schedules.iter_mut().for_each(|resource| {\n    resource.tags.iter_mut().for_each(|id| {\n      let Some(name) = tags.get(id) else {\n        *id = String::new();\n        return;\n      };\n      id.clone_from(name);\n    });\n  });\n  schedules.sort_by(|a, b| {\n    match (a.next_scheduled_run, b.next_scheduled_run) {\n      (Some(_), None) => return Ordering::Less,\n      (None, Some(_)) => return Ordering::Greater,\n      (Some(a), Some(b)) => return a.cmp(&b),\n      (None, None) => {}\n    }\n    a.name.cmp(&b.name).then(a.enabled.cmp(&b.enabled))\n  });\n  if !schedules.is_empty() {\n    print_items(schedules, filters.format, filters.links)?;\n  }\n  Ok(())\n}\n\nfn fix_tags<T>(\n  resources: &mut [ResourceListItem<T>],\n  tags: &HashMap<String, String>,\n) {\n  resources.iter_mut().for_each(|resource| {\n    resource.tags.iter_mut().for_each(|id| {\n      let Some(name) = tags.get(id) else {\n        *id = String::new();\n        return;\n      };\n      id.clone_from(name);\n    });\n  });\n}\n\ntrait ListResources: Sized\nwhere\n  ResourceListItem<Self::Info>: PrintTable,\n{\n  type Info;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    // For use with root `km ls`\n    minimal: bool,\n  ) -> anyhow::Result<Vec<ResourceListItem<Self::Info>>>;\n}\n\n// LIST\n\nimpl ListResources for ServerListItem {\n  type Info = ServerListItemInfo;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    _minimal: bool,\n  ) -> anyhow::Result<Vec<Self>> {\n    let servers = client\n      .read(ListServers {\n        query: ResourceQuery::builder()\n          .tags(filters.tags.clone())\n          // .tag_behavior(TagQueryBehavior::Any)\n          .templates(filters.templates)\n          .build(),\n      })\n      .await?;\n    let names = parse_wildcards(&filters.names);\n    let server_wildcards = parse_wildcards(&filters.servers);\n    let mut servers = servers\n      .into_iter()\n      .filter(|server| {\n        let state_check = if filters.all {\n          true\n        } else if filters.down {\n          !matches!(server.info.state, ServerState::Ok)\n        } else if filters.in_progress {\n          false\n        } else {\n          matches!(server.info.state, ServerState::Ok)\n        };\n        let name_items = &[server.name.as_str()];\n        state_check\n          && matches_wildcards(&names, name_items)\n          && matches_wildcards(&server_wildcards, name_items)\n      })\n      .collect::<Vec<_>>();\n    servers.sort_by(|a, b| {\n      a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))\n    });\n    Ok(servers)\n  }\n}\n\nimpl ListResources for StackListItem {\n  type Info = StackListItemInfo;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    _minimal: bool,\n  ) -> anyhow::Result<Vec<Self>> {\n    let (servers, mut stacks) = tokio::try_join!(\n      client\n        .read(ListServers {\n          query: ResourceQuery::builder().build(),\n        })\n        .map(|res| res.map(|res| res\n          .into_iter()\n          .map(|s| (s.id.clone(), s))\n          .collect::<HashMap<_, _>>())),\n      client.read(ListStacks {\n        query: ResourceQuery::builder()\n          .tags(filters.tags.clone())\n          // .tag_behavior(TagQueryBehavior::Any)\n          .templates(filters.templates)\n          .build(),\n      })\n    )?;\n    stacks.iter_mut().for_each(|stack| {\n      if stack.info.server_id.is_empty() {\n        return;\n      }\n      let Some(server) = servers.get(&stack.info.server_id) else {\n        return;\n      };\n      stack.info.server_id.clone_from(&server.name);\n    });\n    let names = parse_wildcards(&filters.names);\n    let servers = parse_wildcards(&filters.servers);\n    let mut stacks = stacks\n      .into_iter()\n      .filter(|stack| {\n        let state_check = if filters.all {\n          true\n        } else if filters.down {\n          !matches!(\n            stack.info.state,\n            StackState::Running | StackState::Deploying\n          )\n        } else if filters.in_progress {\n          matches!(stack.info.state, StackState::Deploying)\n        } else {\n          matches!(\n            stack.info.state,\n            StackState::Running | StackState::Deploying\n          )\n        };\n        state_check\n          && matches_wildcards(&names, &[stack.name.as_str()])\n          && matches_wildcards(\n            &servers,\n            &[stack.info.server_id.as_str()],\n          )\n      })\n      .collect::<Vec<_>>();\n    stacks.sort_by(|a, b| {\n      a.info\n        .state\n        .cmp(&b.info.state)\n        .then(a.name.cmp(&b.name))\n        .then(a.info.server_id.cmp(&b.info.server_id))\n    });\n    Ok(stacks)\n  }\n}\n\nimpl ListResources for DeploymentListItem {\n  type Info = DeploymentListItemInfo;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    _minimal: bool,\n  ) -> anyhow::Result<Vec<Self>> {\n    let (servers, mut deployments) = tokio::try_join!(\n      client\n        .read(ListServers {\n          query: ResourceQuery::builder().build(),\n        })\n        .map(|res| res.map(|res| res\n          .into_iter()\n          .map(|s| (s.id.clone(), s))\n          .collect::<HashMap<_, _>>())),\n      client.read(ListDeployments {\n        query: ResourceQuery::builder()\n          .tags(filters.tags.clone())\n          // .tag_behavior(TagQueryBehavior::Any)\n          .templates(filters.templates)\n          .build(),\n      })\n    )?;\n    deployments.iter_mut().for_each(|deployment| {\n      if deployment.info.server_id.is_empty() {\n        return;\n      }\n      let Some(server) = servers.get(&deployment.info.server_id)\n      else {\n        return;\n      };\n      deployment.info.server_id.clone_from(&server.name);\n    });\n    let names = parse_wildcards(&filters.names);\n    let servers = parse_wildcards(&filters.servers);\n    let mut deployments = deployments\n      .into_iter()\n      .filter(|deployment| {\n        let state_check = if filters.all {\n          true\n        } else if filters.down {\n          !matches!(\n            deployment.info.state,\n            DeploymentState::Running | DeploymentState::Deploying\n          )\n        } else if filters.in_progress {\n          matches!(deployment.info.state, DeploymentState::Deploying)\n        } else {\n          matches!(\n            deployment.info.state,\n            DeploymentState::Running | DeploymentState::Deploying\n          )\n        };\n        state_check\n          && matches_wildcards(&names, &[deployment.name.as_str()])\n          && matches_wildcards(\n            &servers,\n            &[deployment.info.server_id.as_str()],\n          )\n      })\n      .collect::<Vec<_>>();\n    deployments.sort_by(|a, b| {\n      a.info\n        .state\n        .cmp(&b.info.state)\n        .then(a.name.cmp(&b.name))\n        .then(a.info.server_id.cmp(&b.info.server_id))\n    });\n    Ok(deployments)\n  }\n}\n\nimpl ListResources for BuildListItem {\n  type Info = BuildListItemInfo;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    minimal: bool,\n  ) -> anyhow::Result<Vec<Self>> {\n    let (builders, mut builds) = tokio::try_join!(\n      client\n        .read(ListBuilders {\n          query: ResourceQuery::builder().build(),\n        })\n        .map(|res| res.map(|res| res\n          .into_iter()\n          .map(|s| (s.id.clone(), s))\n          .collect::<HashMap<_, _>>())),\n      client.read(ListBuilds {\n        query: ResourceQuery::builder()\n          .tags(filters.tags.clone())\n          // .tag_behavior(TagQueryBehavior::Any)\n          .templates(filters.templates)\n          .build(),\n      })\n    )?;\n    builds.iter_mut().for_each(|build| {\n      if build.info.builder_id.is_empty() {\n        return;\n      }\n      let Some(builder) = builders.get(&build.info.builder_id) else {\n        return;\n      };\n      build.info.builder_id.clone_from(&builder.name);\n    });\n    let names = parse_wildcards(&filters.names);\n    let builders = parse_wildcards(&filters.builders);\n    let mut builds = builds\n      .into_iter()\n      .filter(|build| {\n        let state_check = if filters.all {\n          true\n        } else if filters.down {\n          matches!(\n            build.info.state,\n            BuildState::Failed | BuildState::Unknown\n          )\n        } else if minimal || filters.in_progress {\n          matches!(build.info.state, BuildState::Building)\n        } else {\n          true\n        };\n        state_check\n          && matches_wildcards(&names, &[build.name.as_str()])\n          && matches_wildcards(\n            &builders,\n            &[build.info.builder_id.as_str()],\n          )\n      })\n      .collect::<Vec<_>>();\n    builds.sort_by(|a, b| {\n      a.name\n        .cmp(&b.name)\n        .then(a.info.builder_id.cmp(&b.info.builder_id))\n        .then(a.info.state.cmp(&b.info.state))\n    });\n    Ok(builds)\n  }\n}\n\nimpl ListResources for RepoListItem {\n  type Info = RepoListItemInfo;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    minimal: bool,\n  ) -> anyhow::Result<Vec<Self>> {\n    let names = parse_wildcards(&filters.names);\n    let mut repos = client\n      .read(ListRepos {\n        query: ResourceQuery::builder()\n          .tags(filters.tags.clone())\n          // .tag_behavior(TagQueryBehavior::Any)\n          .templates(filters.templates)\n          .build(),\n      })\n      .await?\n      .into_iter()\n      .filter(|repo| {\n        let state_check = if filters.all {\n          true\n        } else if filters.down {\n          matches!(\n            repo.info.state,\n            RepoState::Failed | RepoState::Unknown\n          )\n        } else if minimal || filters.in_progress {\n          matches!(\n            repo.info.state,\n            RepoState::Building | RepoState::Cloning\n          )\n        } else {\n          true\n        };\n        state_check\n          && matches_wildcards(&names, &[repo.name.as_str()])\n      })\n      .collect::<Vec<_>>();\n    repos.sort_by(|a, b| {\n      a.name\n        .cmp(&b.name)\n        .then(a.info.server_id.cmp(&b.info.server_id))\n        .then(a.info.builder_id.cmp(&b.info.builder_id))\n    });\n    Ok(repos)\n  }\n}\n\nimpl ListResources for ProcedureListItem {\n  type Info = ProcedureListItemInfo;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    minimal: bool,\n  ) -> anyhow::Result<Vec<Self>> {\n    let names = parse_wildcards(&filters.names);\n    let mut procedures = client\n      .read(ListProcedures {\n        query: ResourceQuery::builder()\n          .tags(filters.tags.clone())\n          // .tag_behavior(TagQueryBehavior::Any)\n          .templates(filters.templates)\n          .build(),\n      })\n      .await?\n      .into_iter()\n      .filter(|procedure| {\n        let state_check = if filters.all {\n          true\n        } else if filters.down {\n          matches!(\n            procedure.info.state,\n            ProcedureState::Failed | ProcedureState::Unknown\n          )\n        } else if minimal || filters.in_progress {\n          matches!(procedure.info.state, ProcedureState::Running)\n        } else {\n          true\n        };\n        state_check\n          && matches_wildcards(&names, &[procedure.name.as_str()])\n      })\n      .collect::<Vec<_>>();\n    procedures.sort_by(|a, b| {\n      match (a.info.next_scheduled_run, b.info.next_scheduled_run) {\n        (Some(_), None) => return Ordering::Less,\n        (None, Some(_)) => return Ordering::Greater,\n        (Some(a), Some(b)) => return a.cmp(&b),\n        (None, None) => {}\n      }\n      a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))\n    });\n    Ok(procedures)\n  }\n}\n\nimpl ListResources for ActionListItem {\n  type Info = ActionListItemInfo;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    minimal: bool,\n  ) -> anyhow::Result<Vec<Self>> {\n    let names = parse_wildcards(&filters.names);\n    let mut actions = client\n      .read(ListActions {\n        query: ResourceQuery::builder()\n          .tags(filters.tags.clone())\n          // .tag_behavior(TagQueryBehavior::Any)\n          .templates(filters.templates)\n          .build(),\n      })\n      .await?\n      .into_iter()\n      .filter(|action| {\n        let state_check = if filters.all {\n          true\n        } else if filters.down {\n          matches!(\n            action.info.state,\n            ActionState::Failed | ActionState::Unknown\n          )\n        } else if minimal || filters.in_progress {\n          matches!(action.info.state, ActionState::Running)\n        } else {\n          true\n        };\n        state_check\n          && matches_wildcards(&names, &[action.name.as_str()])\n      })\n      .collect::<Vec<_>>();\n    actions.sort_by(|a, b| {\n      match (a.info.next_scheduled_run, b.info.next_scheduled_run) {\n        (Some(_), None) => return Ordering::Less,\n        (None, Some(_)) => return Ordering::Greater,\n        (Some(a), Some(b)) => return a.cmp(&b),\n        (None, None) => {}\n      }\n      a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))\n    });\n    Ok(actions)\n  }\n}\n\nimpl ListResources for ResourceSyncListItem {\n  type Info = ResourceSyncListItemInfo;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    minimal: bool,\n  ) -> anyhow::Result<Vec<Self>> {\n    let names = parse_wildcards(&filters.names);\n    let mut syncs = client\n      .read(ListResourceSyncs {\n        query: ResourceQuery::builder()\n          .tags(filters.tags.clone())\n          // .tag_behavior(TagQueryBehavior::Any)\n          .templates(filters.templates)\n          .build(),\n      })\n      .await?\n      .into_iter()\n      .filter(|sync| {\n        let state_check = if filters.all {\n          true\n        } else if filters.down {\n          matches!(\n            sync.info.state,\n            ResourceSyncState::Failed | ResourceSyncState::Unknown\n          )\n        } else if minimal || filters.in_progress {\n          matches!(\n            sync.info.state,\n            ResourceSyncState::Syncing | ResourceSyncState::Pending\n          )\n        } else {\n          true\n        };\n        state_check\n          && matches_wildcards(&names, &[sync.name.as_str()])\n      })\n      .collect::<Vec<_>>();\n    syncs.sort_by(|a, b| {\n      a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))\n    });\n    Ok(syncs)\n  }\n}\n\nimpl ListResources for BuilderListItem {\n  type Info = BuilderListItemInfo;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    minimal: bool,\n  ) -> anyhow::Result<Vec<Self>> {\n    let names = parse_wildcards(&filters.names);\n    let mut builders = client\n      .read(ListBuilders {\n        query: ResourceQuery::builder()\n          .tags(filters.tags.clone())\n          // .tag_behavior(TagQueryBehavior::Any)\n          .templates(filters.templates)\n          .build(),\n      })\n      .await?\n      .into_iter()\n      .filter(|builder| {\n        (!minimal || filters.all)\n          && matches_wildcards(&names, &[builder.name.as_str()])\n      })\n      .collect::<Vec<_>>();\n    builders.sort_by(|a, b| {\n      a.name\n        .cmp(&b.name)\n        .then(a.info.builder_type.cmp(&b.info.builder_type))\n    });\n    Ok(builders)\n  }\n}\n\nimpl ListResources for AlerterListItem {\n  type Info = AlerterListItemInfo;\n  async fn list(\n    client: &KomodoClient,\n    filters: &ResourceFilters,\n    minimal: bool,\n  ) -> anyhow::Result<Vec<Self>> {\n    let names = parse_wildcards(&filters.names);\n    let mut syncs = client\n      .read(ListAlerters {\n        query: ResourceQuery::builder()\n          .tags(filters.tags.clone())\n          // .tag_behavior(TagQueryBehavior::Any)\n          .templates(filters.templates)\n          .build(),\n      })\n      .await?\n      .into_iter()\n      .filter(|sync| {\n        (!minimal || filters.all)\n          && matches_wildcards(&names, &[sync.name.as_str()])\n      })\n      .collect::<Vec<_>>();\n    syncs.sort_by(|a, b| {\n      a.info\n        .enabled\n        .cmp(&b.info.enabled)\n        .then(a.name.cmp(&b.name))\n        .then(a.info.endpoint_type.cmp(&b.info.endpoint_type))\n    });\n    Ok(syncs)\n  }\n}\n\n// TABLE\n\nimpl PrintTable for ResourceListItem<ServerListItemInfo> {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Server\", \"State\", \"Address\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Server\", \"State\", \"Address\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<Cell> {\n    let color = match self.info.state {\n      ServerState::Ok => Color::Green,\n      ServerState::NotOk => Color::Red,\n      ServerState::Disabled => Color::Blue,\n    };\n    let mut res = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.info.state.to_string())\n        .fg(color)\n        .add_attribute(Attribute::Bold),\n      Cell::new(self.info.address),\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      res.push(Cell::new(resource_link(\n        &cli_config().host,\n        ResourceTargetVariant::Server,\n        &self.id,\n      )))\n    }\n    res\n  }\n}\n\nimpl PrintTable for ResourceListItem<StackListItemInfo> {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Stack\", \"State\", \"Server\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Stack\", \"State\", \"Server\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<comfy_table::Cell> {\n    let color = match self.info.state {\n      StackState::Down => Color::Blue,\n      StackState::Running => Color::Green,\n      StackState::Deploying => Color::DarkYellow,\n      StackState::Paused => Color::DarkYellow,\n      StackState::Unknown => Color::Magenta,\n      _ => Color::Red,\n    };\n    // let source = if self.info.files_on_host {\n    //   \"On Host\"\n    // } else if !self.info.repo.is_empty() {\n    //   self.info.repo_link.as_str()\n    // } else {\n    //   \"UI Defined\"\n    // };\n    let mut res = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.info.state.to_string())\n        .fg(color)\n        .add_attribute(Attribute::Bold),\n      Cell::new(self.info.server_id),\n      // Cell::new(source),\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      res.push(Cell::new(resource_link(\n        &cli_config().host,\n        ResourceTargetVariant::Stack,\n        &self.id,\n      )))\n    }\n    res\n  }\n}\n\nimpl PrintTable for ResourceListItem<DeploymentListItemInfo> {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Deployment\", \"State\", \"Server\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Deployment\", \"State\", \"Server\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<comfy_table::Cell> {\n    let color = match self.info.state {\n      DeploymentState::NotDeployed => Color::Blue,\n      DeploymentState::Running => Color::Green,\n      DeploymentState::Deploying => Color::DarkYellow,\n      DeploymentState::Paused => Color::DarkYellow,\n      DeploymentState::Unknown => Color::Magenta,\n      _ => Color::Red,\n    };\n    let mut res = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.info.state.to_string())\n        .fg(color)\n        .add_attribute(Attribute::Bold),\n      Cell::new(self.info.server_id),\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      res.push(Cell::new(resource_link(\n        &cli_config().host,\n        ResourceTargetVariant::Deployment,\n        &self.id,\n      )))\n    }\n    res\n  }\n}\n\nimpl PrintTable for ResourceListItem<BuildListItemInfo> {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Build\", \"State\", \"Builder\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Build\", \"State\", \"Builder\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<comfy_table::Cell> {\n    let color = match self.info.state {\n      BuildState::Ok => Color::Green,\n      BuildState::Building => Color::DarkYellow,\n      BuildState::Unknown => Color::Magenta,\n      BuildState::Failed => Color::Red,\n    };\n    let mut res = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.info.state.to_string())\n        .fg(color)\n        .add_attribute(Attribute::Bold),\n      Cell::new(self.info.builder_id),\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      res.push(Cell::new(resource_link(\n        &cli_config().host,\n        ResourceTargetVariant::Build,\n        &self.id,\n      )));\n    }\n    res\n  }\n}\n\nimpl PrintTable for ResourceListItem<RepoListItemInfo> {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Repo\", \"State\", \"Link\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Repo\", \"State\", \"Link\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<comfy_table::Cell> {\n    let color = match self.info.state {\n      RepoState::Ok => Color::Green,\n      RepoState::Building\n      | RepoState::Cloning\n      | RepoState::Pulling => Color::DarkYellow,\n      RepoState::Unknown => Color::Magenta,\n      RepoState::Failed => Color::Red,\n    };\n    let mut res = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.info.state.to_string())\n        .fg(color)\n        .add_attribute(Attribute::Bold),\n      Cell::new(self.info.repo_link),\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      res.push(Cell::new(resource_link(\n        &cli_config().host,\n        ResourceTargetVariant::Repo,\n        &self.id,\n      )))\n    }\n    res\n  }\n}\n\nimpl PrintTable for ResourceListItem<ProcedureListItemInfo> {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Procedure\", \"State\", \"Next Run\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Procedure\", \"State\", \"Next Run\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<comfy_table::Cell> {\n    let color = match self.info.state {\n      ProcedureState::Ok => Color::Green,\n      ProcedureState::Running => Color::DarkYellow,\n      ProcedureState::Unknown => Color::Magenta,\n      ProcedureState::Failed => Color::Red,\n    };\n    let next_run = if let Some(ts) = self.info.next_scheduled_run {\n      Cell::new(\n        format_timetamp(ts)\n          .unwrap_or(String::from(\"Invalid next ts\")),\n      )\n      .add_attribute(Attribute::Bold)\n    } else {\n      Cell::new(String::from(\"None\"))\n    };\n    let mut res = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.info.state.to_string())\n        .fg(color)\n        .add_attribute(Attribute::Bold),\n      next_run,\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      res.push(Cell::new(resource_link(\n        &cli_config().host,\n        ResourceTargetVariant::Procedure,\n        &self.id,\n      )))\n    }\n    res\n  }\n}\n\nimpl PrintTable for ResourceListItem<ActionListItemInfo> {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Action\", \"State\", \"Next Run\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Action\", \"State\", \"Next Run\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<comfy_table::Cell> {\n    let color = match self.info.state {\n      ActionState::Ok => Color::Green,\n      ActionState::Running => Color::DarkYellow,\n      ActionState::Unknown => Color::Magenta,\n      ActionState::Failed => Color::Red,\n    };\n    let next_run = if let Some(ts) = self.info.next_scheduled_run {\n      Cell::new(\n        format_timetamp(ts)\n          .unwrap_or(String::from(\"Invalid next ts\")),\n      )\n      .add_attribute(Attribute::Bold)\n    } else {\n      Cell::new(String::from(\"None\"))\n    };\n    let mut res = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.info.state.to_string())\n        .fg(color)\n        .add_attribute(Attribute::Bold),\n      next_run,\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      res.push(Cell::new(resource_link(\n        &cli_config().host,\n        ResourceTargetVariant::Action,\n        &self.id,\n      )));\n    }\n    res\n  }\n}\n\nimpl PrintTable for ResourceListItem<ResourceSyncListItemInfo> {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Sync\", \"State\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Sync\", \"State\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<comfy_table::Cell> {\n    let color = match self.info.state {\n      ResourceSyncState::Ok => Color::Green,\n      ResourceSyncState::Pending | ResourceSyncState::Syncing => {\n        Color::DarkYellow\n      }\n      ResourceSyncState::Unknown => Color::Magenta,\n      ResourceSyncState::Failed => Color::Red,\n    };\n    let mut res = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.info.state.to_string())\n        .fg(color)\n        .add_attribute(Attribute::Bold),\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      res.push(Cell::new(resource_link(\n        &cli_config().host,\n        ResourceTargetVariant::ResourceSync,\n        &self.id,\n      )))\n    }\n    res\n  }\n}\n\nimpl PrintTable for ResourceListItem<BuilderListItemInfo> {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Builder\", \"Type\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Builder\", \"Type\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<comfy_table::Cell> {\n    let mut res = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.info.builder_type),\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      res.push(Cell::new(resource_link(\n        &cli_config().host,\n        ResourceTargetVariant::Builder,\n        &self.id,\n      )));\n    }\n    res\n  }\n}\n\nimpl PrintTable for ResourceListItem<AlerterListItemInfo> {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Alerter\", \"Type\", \"Enabled\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Alerter\", \"Type\", \"Enabled\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<comfy_table::Cell> {\n    let mut row = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.info.endpoint_type),\n      if self.info.enabled {\n        Cell::new(self.info.enabled.to_string()).fg(Color::Green)\n      } else {\n        Cell::new(self.info.enabled.to_string()).fg(Color::Red)\n      },\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      row.push(Cell::new(resource_link(\n        &cli_config().host,\n        ResourceTargetVariant::Alerter,\n        &self.id,\n      )));\n    }\n    row\n  }\n}\n\nimpl PrintTable for Schedule {\n  fn header(links: bool) -> &'static [&'static str] {\n    if links {\n      &[\"Name\", \"Type\", \"Next Run\", \"Tags\", \"Link\"]\n    } else {\n      &[\"Name\", \"Type\", \"Next Run\", \"Tags\"]\n    }\n  }\n  fn row(self, links: bool) -> Vec<comfy_table::Cell> {\n    let next_run = if let Some(ts) = self.next_scheduled_run {\n      Cell::new(\n        format_timetamp(ts)\n          .unwrap_or(String::from(\"Invalid next ts\")),\n      )\n      .add_attribute(Attribute::Bold)\n    } else {\n      Cell::new(String::from(\"None\"))\n    };\n    let (resource_type, id) = self.target.extract_variant_id();\n    let mut res = vec![\n      Cell::new(self.name).add_attribute(Attribute::Bold),\n      Cell::new(self.target.extract_variant_id().0),\n      next_run,\n      Cell::new(self.tags.join(\", \")),\n    ];\n    if links {\n      res.push(Cell::new(resource_link(\n        &cli_config().host,\n        resource_type,\n        id,\n      )));\n    }\n    res\n  }\n}\n"
  },
  {
    "path": "bin/cli/src/command/mod.rs",
    "content": "use std::io::Read;\n\nuse anyhow::{Context, anyhow};\nuse chrono::TimeZone;\nuse colored::Colorize;\nuse comfy_table::{Attribute, Cell, Table};\nuse komodo_client::{\n  KomodoClient,\n  entities::config::cli::{CliTableBorders, args::CliFormat},\n};\nuse serde::Serialize;\nuse tokio::sync::OnceCell;\nuse wildcard::Wildcard;\n\nuse crate::config::cli_config;\n\npub mod container;\npub mod database;\npub mod execute;\npub mod list;\npub mod update;\n\nasync fn komodo_client() -> anyhow::Result<&'static KomodoClient> {\n  static KOMODO_CLIENT: OnceCell<KomodoClient> =\n    OnceCell::const_new();\n  KOMODO_CLIENT\n    .get_or_try_init(|| async {\n      let config = cli_config();\n      let (Some(key), Some(secret)) =\n        (&config.cli_key, &config.cli_secret)\n      else {\n        return Err(anyhow!(\n          \"Must provide both cli_key and cli_secret\"\n        ));\n      };\n      KomodoClient::new(&config.host, key, secret)\n        .with_healthcheck()\n        .await\n    })\n    .await\n}\n\nfn wait_for_enter(\n  press_enter_to: &str,\n  skip: bool,\n) -> anyhow::Result<()> {\n  if skip {\n    println!();\n    return Ok(());\n  }\n  println!(\n    \"\\nPress {} to {}\\n\",\n    \"ENTER\".green(),\n    press_enter_to.bold()\n  );\n  let buffer = &mut [0u8];\n  std::io::stdin()\n    .read_exact(buffer)\n    .context(\"failed to read ENTER\")?;\n  Ok(())\n}\n\n/// Sanitizes uris of the form:\n/// `protocol://username:password@address`\nfn sanitize_uri(uri: &str) -> String {\n  // protocol: `mongodb`\n  // credentials_address: `username:password@address`\n  let Some((protocol, credentials_address)) = uri.split_once(\"://\")\n  else {\n    // If no protocol, return as-is\n    return uri.to_string();\n  };\n\n  // credentials: `username:password`\n  let Some((credentials, address)) =\n    credentials_address.split_once('@')\n  else {\n    // If no credentials, return as-is\n    return uri.to_string();\n  };\n\n  match credentials.split_once(':') {\n    Some((username, _)) => {\n      format!(\"{protocol}://{username}:*****@{address}\")\n    }\n    None => {\n      format!(\"{protocol}://*****@{address}\")\n    }\n  }\n}\n\nfn print_items<T: PrintTable + Serialize>(\n  items: Vec<T>,\n  format: CliFormat,\n  links: bool,\n) -> anyhow::Result<()> {\n  match format {\n    CliFormat::Table => {\n      let mut table = Table::new();\n      let preset = {\n        use comfy_table::presets::*;\n        match cli_config().table_borders {\n          None | Some(CliTableBorders::Horizontal) => {\n            UTF8_HORIZONTAL_ONLY\n          }\n          Some(CliTableBorders::Vertical) => UTF8_FULL_CONDENSED,\n          Some(CliTableBorders::Inside) => UTF8_NO_BORDERS,\n          Some(CliTableBorders::Outside) => UTF8_BORDERS_ONLY,\n          Some(CliTableBorders::All) => UTF8_FULL,\n        }\n      };\n      table.load_preset(preset).set_header(\n        T::header(links)\n          .iter()\n          .map(|h| Cell::new(h).add_attribute(Attribute::Bold)),\n      );\n      for item in items {\n        table.add_row(item.row(links));\n      }\n      println!(\"{table}\");\n    }\n    CliFormat::Json => {\n      println!(\n        \"{}\",\n        serde_json::to_string_pretty(&items)\n          .context(\"Failed to serialize items to JSON\")?\n      );\n    }\n  }\n  Ok(())\n}\n\ntrait PrintTable {\n  fn header(links: bool) -> &'static [&'static str];\n  fn row(self, links: bool) -> Vec<Cell>;\n}\n\nfn parse_wildcards(items: &[String]) -> Vec<Wildcard<'_>> {\n  items\n    .iter()\n    .flat_map(|i| {\n      Wildcard::new(i.as_bytes()).inspect_err(|e| {\n        warn!(\"Failed to parse wildcard: {i} | {e:?}\")\n      })\n    })\n    .collect::<Vec<_>>()\n}\n\nfn matches_wildcards(\n  wildcards: &[Wildcard<'_>],\n  items: &[&str],\n) -> bool {\n  if wildcards.is_empty() {\n    return true;\n  }\n  items.iter().any(|item| {\n    wildcards.iter().any(|wc| wc.is_match(item.as_bytes()))\n  })\n}\n\nfn format_timetamp(ts: i64) -> anyhow::Result<String> {\n  let ts = chrono::Local\n    .timestamp_millis_opt(ts)\n    .single()\n    .context(\"Invalid ts\")?\n    .format(\"%m/%d %H:%M:%S\")\n    .to_string();\n  Ok(ts)\n}\n\nfn clamp_sha(maybe_sha: &str) -> String {\n  if maybe_sha.starts_with(\"sha256:\") {\n    maybe_sha[0..20].to_string() + \"...\"\n  } else {\n    maybe_sha.to_string()\n  }\n}\n\n// fn text_link(link: &str, text: &str) -> String {\n//   format!(\"\\x1b]8;;{link}\\x07{text}\\x1b]8;;\\x07\")\n// }\n"
  },
  {
    "path": "bin/cli/src/command/update/mod.rs",
    "content": "use komodo_client::entities::{\n  build::PartialBuildConfig,\n  config::cli::args::update::UpdateCommand,\n  deployment::PartialDeploymentConfig, repo::PartialRepoConfig,\n  server::PartialServerConfig, stack::PartialStackConfig,\n  sync::PartialResourceSyncConfig,\n};\n\nmod resource;\nmod user;\nmod variable;\n\npub async fn handle(command: &UpdateCommand) -> anyhow::Result<()> {\n  match command {\n    UpdateCommand::Build(update) => {\n      resource::update::<PartialBuildConfig>(update).await\n    }\n    UpdateCommand::Deployment(update) => {\n      resource::update::<PartialDeploymentConfig>(update).await\n    }\n    UpdateCommand::Repo(update) => {\n      resource::update::<PartialRepoConfig>(update).await\n    }\n    UpdateCommand::Server(update) => {\n      resource::update::<PartialServerConfig>(update).await\n    }\n    UpdateCommand::Stack(update) => {\n      resource::update::<PartialStackConfig>(update).await\n    }\n    UpdateCommand::Sync(update) => {\n      resource::update::<PartialResourceSyncConfig>(update).await\n    }\n    UpdateCommand::Variable {\n      name,\n      value,\n      secret,\n      yes,\n    } => variable::update(name, value, *secret, *yes).await,\n    UpdateCommand::User { username, command } => {\n      user::update(username, command).await\n    }\n  }\n}\n"
  },
  {
    "path": "bin/cli/src/command/update/resource.rs",
    "content": "use anyhow::Context;\nuse colored::Colorize;\nuse komodo_client::{\n  api::write::{\n    UpdateBuild, UpdateDeployment, UpdateRepo, UpdateResourceSync,\n    UpdateServer, UpdateStack,\n  },\n  entities::{\n    build::PartialBuildConfig,\n    config::cli::args::update::UpdateResource,\n    deployment::PartialDeploymentConfig, repo::PartialRepoConfig,\n    server::PartialServerConfig, stack::PartialStackConfig,\n    sync::PartialResourceSyncConfig,\n  },\n};\nuse serde::{Serialize, de::DeserializeOwned};\n\npub async fn update<\n  T: std::fmt::Debug + Serialize + DeserializeOwned + ResourceUpdate,\n>(\n  UpdateResource {\n    resource,\n    update,\n    yes,\n  }: &UpdateResource,\n) -> anyhow::Result<()> {\n  println!(\"\\n{}: Update {}\\n\", \"Mode\".dimmed(), T::resource_type());\n  println!(\" - {}: {resource}\", \"Name\".dimmed());\n\n  let config = serde_qs::from_str::<T>(update)\n    .context(\"Failed to deserialize config\")?;\n\n  match serde_json::to_string_pretty(&config) {\n    Ok(config) => {\n      println!(\" - {}: {config}\", \"Update\".dimmed());\n    }\n    Err(_) => {\n      println!(\" - {}: {config:#?}\", \"Update\".dimmed());\n    }\n  }\n\n  crate::command::wait_for_enter(\"update resource\", *yes)?;\n\n  config.apply(resource).await\n}\n\npub trait ResourceUpdate {\n  fn resource_type() -> &'static str;\n  async fn apply(self, resource: &str) -> anyhow::Result<()>;\n}\n\nimpl ResourceUpdate for PartialBuildConfig {\n  fn resource_type() -> &'static str {\n    \"Build\"\n  }\n  async fn apply(self, resource: &str) -> anyhow::Result<()> {\n    let client = crate::command::komodo_client().await?;\n    client\n      .write(UpdateBuild {\n        id: resource.to_string(),\n        config: self,\n      })\n      .await\n      .context(\"Failed to update build config\")?;\n    Ok(())\n  }\n}\n\nimpl ResourceUpdate for PartialDeploymentConfig {\n  fn resource_type() -> &'static str {\n    \"Deployment\"\n  }\n  async fn apply(self, resource: &str) -> anyhow::Result<()> {\n    let client = crate::command::komodo_client().await?;\n    client\n      .write(UpdateDeployment {\n        id: resource.to_string(),\n        config: self,\n      })\n      .await\n      .context(\"Failed to update deployment config\")?;\n    Ok(())\n  }\n}\n\nimpl ResourceUpdate for PartialRepoConfig {\n  fn resource_type() -> &'static str {\n    \"Repo\"\n  }\n  async fn apply(self, resource: &str) -> anyhow::Result<()> {\n    let client = crate::command::komodo_client().await?;\n    client\n      .write(UpdateRepo {\n        id: resource.to_string(),\n        config: self,\n      })\n      .await\n      .context(\"Failed to update repo config\")?;\n    Ok(())\n  }\n}\n\nimpl ResourceUpdate for PartialServerConfig {\n  fn resource_type() -> &'static str {\n    \"Server\"\n  }\n  async fn apply(self, resource: &str) -> anyhow::Result<()> {\n    let client = crate::command::komodo_client().await?;\n    client\n      .write(UpdateServer {\n        id: resource.to_string(),\n        config: self,\n      })\n      .await\n      .context(\"Failed to update server config\")?;\n    Ok(())\n  }\n}\n\nimpl ResourceUpdate for PartialStackConfig {\n  fn resource_type() -> &'static str {\n    \"Stack\"\n  }\n  async fn apply(self, resource: &str) -> anyhow::Result<()> {\n    let client = crate::command::komodo_client().await?;\n    client\n      .write(UpdateStack {\n        id: resource.to_string(),\n        config: self,\n      })\n      .await\n      .context(\"Failed to update stack config\")?;\n    Ok(())\n  }\n}\n\nimpl ResourceUpdate for PartialResourceSyncConfig {\n  fn resource_type() -> &'static str {\n    \"Sync\"\n  }\n  async fn apply(self, resource: &str) -> anyhow::Result<()> {\n    let client = crate::command::komodo_client().await?;\n    client\n      .write(UpdateResourceSync {\n        id: resource.to_string(),\n        config: self,\n      })\n      .await\n      .context(\"Failed to update sync config\")?;\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "bin/cli/src/command/update/user.rs",
    "content": "use anyhow::Context;\nuse colored::Colorize;\nuse database::mungos::mongodb::bson::doc;\nuse komodo_client::entities::{\n  config::{\n    cli::args::{CliEnabled, update::UpdateUserCommand},\n    empty_or_redacted,\n  },\n  optional_string,\n};\n\nuse crate::{command::sanitize_uri, config::cli_config};\n\npub async fn update(\n  username: &str,\n  command: &UpdateUserCommand,\n) -> anyhow::Result<()> {\n  match command {\n    UpdateUserCommand::Password {\n      password,\n      unsanitized,\n      yes,\n    } => {\n      update_password(username, password, *unsanitized, *yes).await\n    }\n    UpdateUserCommand::SuperAdmin { enabled, yes } => {\n      update_super_admin(username, *enabled, *yes).await\n    }\n  }\n}\n\nasync fn update_password(\n  username: &str,\n  password: &str,\n  unsanitized: bool,\n  yes: bool,\n) -> anyhow::Result<()> {\n  println!(\"\\n{}: Update Password\\n\", \"Mode\".dimmed());\n  println!(\" - {}: {username}\", \"Username\".dimmed());\n  if unsanitized {\n    println!(\" - {}: {password}\", \"Password\".dimmed());\n  } else {\n    println!(\n      \" - {}: {}\",\n      \"Password\".dimmed(),\n      empty_or_redacted(password)\n    );\n  }\n\n  crate::command::wait_for_enter(\"update password\", yes)?;\n\n  info!(\"Updating password...\");\n\n  let db = database::Client::new(&cli_config().database).await?;\n\n  let user = db\n    .users\n    .find_one(doc! { \"username\": username })\n    .await\n    .context(\"Failed to query database for user\")?\n    .context(\"No user found with given username\")?;\n\n  db.set_user_password(&user, password).await?;\n\n  info!(\"Password updated ✅\");\n\n  Ok(())\n}\n\nasync fn update_super_admin(\n  username: &str,\n  super_admin: CliEnabled,\n  yes: bool,\n) -> anyhow::Result<()> {\n  let config = cli_config();\n\n  println!(\"\\n{}: Update Super Admin\\n\", \"Mode\".dimmed());\n  println!(\" - {}: {username}\", \"Username\".dimmed());\n  println!(\" - {}: {super_admin}\\n\", \"Super Admin\".dimmed());\n\n  if let Some(uri) = optional_string(&config.database.uri) {\n    println!(\"{}: {}\", \" - Source URI\".dimmed(), sanitize_uri(&uri));\n  }\n  if let Some(address) = optional_string(&config.database.address) {\n    println!(\"{}: {address}\", \" - Source Address\".dimmed());\n  }\n  if let Some(username) = optional_string(&config.database.username) {\n    println!(\"{}: {username}\", \" - Source Username\".dimmed());\n  }\n  println!(\n    \"{}: {}\",\n    \" - Source Db Name\".dimmed(),\n    config.database.db_name,\n  );\n\n  crate::command::wait_for_enter(\"update super admin\", yes)?;\n\n  info!(\"Updating super admin...\");\n\n  let db = database::Client::new(&config.database).await?;\n\n  // Make sure the user exists first before saying it is successful.\n  let user = db\n    .users\n    .find_one(doc! { \"username\": username })\n    .await\n    .context(\"Failed to query database for user\")?\n    .context(\"No user found with given username\")?;\n\n  let super_admin: bool = super_admin.into();\n  db.users\n    .update_one(\n      doc! { \"username\": user.username },\n      doc! { \"$set\": { \"super_admin\": super_admin } },\n    )\n    .await\n    .context(\"Failed to update user super admin on db\")?;\n\n  info!(\"Super admin updated ✅\");\n\n  Ok(())\n}\n"
  },
  {
    "path": "bin/cli/src/command/update/variable.rs",
    "content": "use anyhow::Context;\nuse colored::Colorize;\nuse komodo_client::api::{\n  read::GetVariable,\n  write::{\n    CreateVariable, UpdateVariableIsSecret, UpdateVariableValue,\n  },\n};\n\npub async fn update(\n  name: &str,\n  value: &str,\n  secret: Option<bool>,\n  yes: bool,\n) -> anyhow::Result<()> {\n  println!(\"\\n{}: Update Variable\\n\", \"Mode\".dimmed());\n  println!(\" - {}:  {name}\", \"Name\".dimmed());\n  println!(\" - {}: {value}\", \"Value\".dimmed());\n  if let Some(secret) = secret {\n    println!(\" - {}: {secret}\", \"Is Secret\".dimmed());\n  }\n\n  crate::command::wait_for_enter(\"update variable\", yes)?;\n\n  let client = crate::command::komodo_client().await?;\n\n  let Ok(existing) = client\n    .read(GetVariable {\n      name: name.to_string(),\n    })\n    .await\n  else {\n    // Create the variable\n    client\n      .write(CreateVariable {\n        name: name.to_string(),\n        value: value.to_string(),\n        is_secret: secret.unwrap_or_default(),\n        description: Default::default(),\n      })\n      .await\n      .context(\"Failed to create variable\")?;\n    info!(\"Variable created ✅\");\n    return Ok(());\n  };\n\n  client\n    .write(UpdateVariableValue {\n      name: name.to_string(),\n      value: value.to_string(),\n    })\n    .await\n    .context(\"Failed to update variable 'value'\")?;\n  info!(\"Variable 'value' updated ✅\");\n\n  let Some(secret) = secret else { return Ok(()) };\n\n  if secret != existing.is_secret {\n    client\n      .write(UpdateVariableIsSecret {\n        name: name.to_string(),\n        is_secret: secret,\n      })\n      .await\n      .context(\"Failed to update variable 'is_secret'\")?;\n    info!(\"Variable 'is_secret' updated to {secret} ✅\");\n  }\n\n  Ok(())\n}\n"
  },
  {
    "path": "bin/cli/src/config.rs",
    "content": "use std::{path::PathBuf, sync::OnceLock};\n\nuse anyhow::Context;\nuse clap::Parser;\nuse colored::Colorize;\nuse environment_file::maybe_read_item_from_file;\nuse komodo_client::entities::{\n  config::{\n    DatabaseConfig,\n    cli::{\n      CliConfig, Env,\n      args::{CliArgs, Command, Execute, database::DatabaseCommand},\n    },\n  },\n  logger::LogConfig,\n};\n\npub fn cli_args() -> &'static CliArgs {\n  static CLI_ARGS: OnceLock<CliArgs> = OnceLock::new();\n  CLI_ARGS.get_or_init(CliArgs::parse)\n}\n\npub fn cli_env() -> &'static Env {\n  static CLI_ARGS: OnceLock<Env> = OnceLock::new();\n  CLI_ARGS.get_or_init(|| {\n    match envy::from_env()\n      .context(\"Failed to parse Komodo CLI environment\")\n    {\n      Ok(env) => env,\n      Err(e) => {\n        panic!(\"{e:?}\");\n      }\n    }\n  })\n}\n\npub fn cli_config() -> &'static CliConfig {\n  static CLI_CONFIG: OnceLock<CliConfig> = OnceLock::new();\n  CLI_CONFIG.get_or_init(|| {\n    let args = cli_args();\n    let env = cli_env().clone();\n    let config_paths = args\n      .config_path\n      .clone()\n      .unwrap_or(env.komodo_cli_config_paths);\n    let debug_startup =\n      args.debug_startup.unwrap_or(env.komodo_cli_debug_startup);\n\n    if debug_startup {\n      println!(\n        \"{}: Komodo CLI version: {}\",\n        \"DEBUG\".cyan(),\n        env!(\"CARGO_PKG_VERSION\").blue().bold()\n      );\n      println!(\n        \"{}: {}: {config_paths:?}\",\n        \"DEBUG\".cyan(),\n        \"Config Paths\".dimmed(),\n      );\n    }\n\n    let config_keywords = args\n      .config_keyword\n      .clone()\n      .unwrap_or(env.komodo_cli_config_keywords);\n    let config_keywords = config_keywords\n      .iter()\n      .map(String::as_str)\n      .collect::<Vec<_>>();\n    if debug_startup {\n      println!(\n        \"{}: {}: {config_keywords:?}\",\n        \"DEBUG\".cyan(),\n        \"Config File Keywords\".dimmed(),\n      );\n    }\n    let mut unparsed_config = (config::ConfigLoader {\n      paths: &config_paths\n        .iter()\n        .map(PathBuf::as_path)\n        .collect::<Vec<_>>(),\n      match_wildcards: &config_keywords,\n      include_file_name: \".kminclude\",\n      merge_nested: env.komodo_cli_merge_nested_config,\n      extend_array: env.komodo_cli_extend_config_arrays,\n      debug_print: debug_startup,\n    })\n    .load::<serde_json::Map<String, serde_json::Value>>()\n    .expect(\"failed at parsing config from paths\");\n    let init_parsed_config = serde_json::from_value::<CliConfig>(\n      serde_json::Value::Object(unparsed_config.clone()),\n    )\n    .context(\"Failed to parse config\")\n    .unwrap();\n\n    let (host, key, secret) = match &args.command {\n      Command::Execute(Execute {\n        host, key, secret, ..\n      }) => (host.clone(), key.clone(), secret.clone()),\n      _ => (None, None, None),\n    };\n\n    let backups_folder = match &args.command {\n      Command::Database {\n        command: DatabaseCommand::Backup { backups_folder, .. },\n      } => backups_folder.clone(),\n      Command::Database {\n        command: DatabaseCommand::Restore { backups_folder, .. },\n      } => backups_folder.clone(),\n      _ => None,\n    };\n    let (uri, address, username, password, db_name) =\n      match &args.command {\n        Command::Database {\n          command:\n            DatabaseCommand::Copy {\n              uri,\n              address,\n              username,\n              password,\n              db_name,\n              ..\n            },\n        } => (\n          uri.clone(),\n          address.clone(),\n          username.clone(),\n          password.clone(),\n          db_name.clone(),\n        ),\n        _ => (None, None, None, None, None),\n      };\n\n    let profile = args\n      .profile\n      .as_ref()\n      .or(init_parsed_config.default_profile.as_ref());\n\n    let unparsed_config = if let Some(profile) = profile\n      && !profile.is_empty()\n    {\n      // Find the profile config,\n      // then merge it with the Default config.\n      let serde_json::Value::Array(profiles) = unparsed_config\n        .remove(\"profile\")\n        .context(\"Config has no profiles, but a profile is required\")\n        .unwrap()\n      else {\n        panic!(\"`config.profile` is not array\");\n      };\n      let Some(profile_config) = profiles.into_iter().find(|p| {\n        let Ok(parsed) =\n          serde_json::from_value::<CliConfig>(p.clone())\n        else {\n          return false;\n        };\n        &parsed.config_profile == profile\n          || parsed\n            .config_aliases\n            .iter()\n            .any(|alias| alias == profile)\n      }) else {\n        panic!(\"No profile matching '{profile}' was found.\");\n      };\n      let serde_json::Value::Object(profile_config) = profile_config\n      else {\n        panic!(\"Profile config is not Object type.\");\n      };\n      config::merge_config(\n        unparsed_config,\n        profile_config.clone(),\n        env.komodo_cli_merge_nested_config,\n        env.komodo_cli_extend_config_arrays,\n      )\n      .unwrap_or(profile_config)\n    } else {\n      unparsed_config\n    };\n    let config = serde_json::from_value::<CliConfig>(\n      serde_json::Value::Object(unparsed_config),\n    )\n    .context(\"Failed to parse final config\")\n    .unwrap();\n    let config_profile = if config.config_profile.is_empty() {\n      String::from(\"None\")\n    } else {\n      config.config_profile\n    };\n\n    CliConfig {\n      config_profile,\n      config_aliases: config.config_aliases,\n      default_profile: config.default_profile,\n      table_borders: env\n        .komodo_cli_table_borders\n        .or(config.table_borders),\n      host: host\n        .or(env.komodo_cli_host)\n        .or(env.komodo_host)\n        .unwrap_or(config.host),\n      cli_key: key.or(env.komodo_cli_key).or(config.cli_key),\n      cli_secret: secret\n        .or(env.komodo_cli_secret)\n        .or(config.cli_secret),\n      backups_folder: backups_folder\n        .or(env.komodo_cli_backups_folder)\n        .unwrap_or(config.backups_folder),\n      max_backups: env\n        .komodo_cli_max_backups\n        .unwrap_or(config.max_backups),\n      database_target: DatabaseConfig {\n        uri: uri\n          .or(env.komodo_cli_database_target_uri)\n          .unwrap_or(config.database_target.uri),\n        address: address\n          .or(env.komodo_cli_database_target_address)\n          .unwrap_or(config.database_target.address),\n        username: username\n          .or(env.komodo_cli_database_target_username)\n          .unwrap_or(config.database_target.username),\n        password: password\n          .or(env.komodo_cli_database_target_password)\n          .unwrap_or(config.database_target.password),\n        db_name: db_name\n          .or(env.komodo_cli_database_target_db_name)\n          .unwrap_or(config.database_target.db_name),\n        app_name: config.database_target.app_name,\n      },\n      database: DatabaseConfig {\n        uri: maybe_read_item_from_file(\n          env.komodo_database_uri_file,\n          env.komodo_database_uri,\n        )\n        .unwrap_or(config.database.uri),\n        address: env\n          .komodo_database_address\n          .unwrap_or(config.database.address),\n        username: maybe_read_item_from_file(\n          env.komodo_database_username_file,\n          env.komodo_database_username,\n        )\n        .unwrap_or(config.database.username),\n        password: maybe_read_item_from_file(\n          env.komodo_database_password_file,\n          env.komodo_database_password,\n        )\n        .unwrap_or(config.database.password),\n        db_name: env\n          .komodo_database_db_name\n          .unwrap_or(config.database.db_name),\n        app_name: config.database.app_name,\n      },\n      cli_logging: LogConfig {\n        level: env\n          .komodo_cli_logging_level\n          .unwrap_or(config.cli_logging.level),\n        stdio: env\n          .komodo_cli_logging_stdio\n          .unwrap_or(config.cli_logging.stdio),\n        pretty: env\n          .komodo_cli_logging_pretty\n          .unwrap_or(config.cli_logging.pretty),\n        location: false,\n        otlp_endpoint: env\n          .komodo_cli_logging_otlp_endpoint\n          .unwrap_or(config.cli_logging.otlp_endpoint),\n        opentelemetry_service_name: env\n          .komodo_cli_logging_opentelemetry_service_name\n          .unwrap_or(config.cli_logging.opentelemetry_service_name),\n      },\n      profile: config.profile,\n    }\n  })\n}\n"
  },
  {
    "path": "bin/cli/src/main.rs",
    "content": "#[macro_use]\nextern crate tracing;\n\nuse anyhow::Context;\nuse komodo_client::entities::config::cli::args;\n\nuse crate::config::cli_config;\n\nmod command;\nmod config;\n\nasync fn app() -> anyhow::Result<()> {\n  dotenvy::dotenv().ok();\n  logger::init(&config::cli_config().cli_logging)?;\n  let args = config::cli_args();\n  let env = config::cli_env();\n  let debug_load =\n    args.debug_startup.unwrap_or(env.komodo_cli_debug_startup);\n\n  match &args.command {\n    args::Command::Config {\n      all_profiles,\n      unsanitized,\n    } => {\n      let mut config = if *unsanitized {\n        cli_config().clone()\n      } else {\n        cli_config().sanitized()\n      };\n      if !*all_profiles {\n        config.profile = Default::default();\n      }\n      if debug_load {\n        println!(\"\\n{config:#?}\");\n      } else {\n        println!(\n          \"\\nCLI Config {}\",\n          serde_json::to_string_pretty(&config)\n            .context(\"Failed to serialize config for pretty print\")?\n        );\n      }\n      Ok(())\n    }\n    args::Command::Container(container) => {\n      command::container::handle(container).await\n    }\n    args::Command::Inspect(inspect) => {\n      command::container::inspect_container(inspect).await\n    }\n    args::Command::List(list) => command::list::handle(list).await,\n    args::Command::Execute(args) => {\n      command::execute::handle(&args.execution, args.yes).await\n    }\n    args::Command::Update { command } => {\n      command::update::handle(command).await\n    }\n    args::Command::Database { command } => {\n      command::database::handle(command).await\n    }\n  }\n}\n\n#[tokio::main]\nasync fn main() -> anyhow::Result<()> {\n  let mut term_signal = tokio::signal::unix::signal(\n    tokio::signal::unix::SignalKind::terminate(),\n  )?;\n  tokio::select! {\n    res = tokio::spawn(app()) => res?,\n    _ = term_signal.recv() => Ok(()),\n  }\n}\n"
  },
  {
    "path": "bin/core/Cargo.toml",
    "content": "[package]\nname = \"komodo_core\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\n\n[[bin]]\nname = \"core\"\npath = \"src/main.rs\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n# local\nkomodo_client = { workspace = true, features = [\"mongo\"] }\nperiphery_client.workspace = true\nenvironment_file.workspace = true\ninterpolate.workspace = true\nformatting.workspace = true\ndatabase.workspace = true\nresponse.workspace = true\ncommand.workspace = true\nconfig.workspace = true\nlogger.workspace = true\ncache.workspace = true\ngit.workspace = true\n# mogh\nserror = { workspace = true, features = [\"axum\"] }\nasync_timing_util.workspace = true\npartial_derive2.workspace = true\nderive_variants.workspace = true\nresolver_api.workspace = true\ntoml_pretty.workspace = true\nslack.workspace = true\nsvi.workspace = true\n# external\naws-credential-types.workspace = true\ntokio-tungstenite.workspace = true\nenglish-to-cron.workspace = true\nopenidconnect.workspace = true\njsonwebtoken.workspace = true\naxum-server.workspace = true\nurlencoding.workspace = true\naws-sdk-ec2.workspace = true\naws-config.workspace = true\ntokio-util.workspace = true\naxum-extra.workspace = true\ntower-http.workspace = true\nserde_json.workspace = true\nserde_yaml_ng.workspace = true\ntypeshare.workspace = true\nchrono-tz.workspace = true\nindexmap.workspace = true\noctorust.workspace = true\nwildcard.workspace = true\narc-swap.workspace = true\ncolored.workspace = true\ndashmap.workspace = true\ntracing.workspace = true\nreqwest.workspace = true\nfutures.workspace = true\nnom_pem.workspace = true\ndotenvy.workspace = true\nanyhow.workspace = true\ncroner.workspace = true\nchrono.workspace = true\nbcrypt.workspace = true\nbase64.workspace = true\nrustls.workspace = true\ntokio.workspace = true\nserde.workspace = true\nregex.workspace = true\naxum.workspace = true\ntoml.workspace = true\nuuid.workspace = true\nenvy.workspace = true\nrand.workspace = true\nhmac.workspace = true\nsha2.workspace = true\nhex.workspace = true\n"
  },
  {
    "path": "bin/core/aio.Dockerfile",
    "content": "## All in one, multi stage compile + runtime Docker build for your architecture.\n\n# Build Core\nFROM rust:1.89.0-bullseye AS core-builder\nRUN cargo install cargo-strip\n\nWORKDIR /builder\nCOPY Cargo.toml Cargo.lock ./\nCOPY ./lib ./lib\nCOPY ./client/core/rs ./client/core/rs\nCOPY ./client/periphery ./client/periphery\nCOPY ./bin/core ./bin/core\nCOPY ./bin/cli ./bin/cli\n\n# Compile app\nRUN cargo build -p komodo_core --release && \\\n  cargo build -p komodo_cli --release && \\\n  cargo strip\n\n# Build Frontend\nFROM node:20.12-alpine AS frontend-builder\nWORKDIR /builder\nCOPY ./frontend ./frontend\nCOPY ./client/core/ts ./client\nRUN cd client && yarn && yarn build && yarn link\nRUN cd frontend && yarn link komodo_client && yarn && yarn build\n\n# Final Image\nFROM debian:bullseye-slim\n\nCOPY ./bin/core/starship.toml /starship.toml\nCOPY ./bin/core/debian-deps.sh .\nRUN sh ./debian-deps.sh && rm ./debian-deps.sh\n\n# Setup an application directory\nWORKDIR /app\n\n# Copy\nCOPY ./config/core.config.toml /config/.default.config.toml\nCOPY --from=frontend-builder /builder/frontend/dist /app/frontend\nCOPY --from=core-builder /builder/target/release/core /usr/local/bin/core\nCOPY --from=core-builder /builder/target/release/km /usr/local/bin/km\nCOPY --from=denoland/deno:bin /deno /usr/local/bin/deno\n\n# Set $DENO_DIR and preload external Deno deps\nENV DENO_DIR=/action-cache/deno\nRUN mkdir /action-cache && \\\n  cd /action-cache && \\\n  deno install jsr:@std/yaml jsr:@std/toml\n\n# Hint at the port\nEXPOSE 9120\n\nENV KOMODO_CLI_CONFIG_PATHS=\"/config\"\n# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`\nENV KOMODO_CLI_CONFIG_KEYWORDS=\"*config.*,*komodo.cli*.*\"\n\nCMD [ \"core\" ]\n\n# Label for Ghcr\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo Core\"\nLABEL org.opencontainers.image.licenses=GPL-3.0\n"
  },
  {
    "path": "bin/core/debian-deps.sh",
    "content": "#!/bin/bash\n\n## Core deps installer\n\napt-get update\napt-get install -y git curl ca-certificates iproute2\n\nrm -rf /var/lib/apt/lists/*\n\n# Starship prompt\ncurl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin\necho 'export STARSHIP_CONFIG=/starship.toml' >> /root/.bashrc\necho 'eval \"$(starship init bash)\"' >> /root/.bashrc\n\n"
  },
  {
    "path": "bin/core/multi-arch.Dockerfile",
    "content": "## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).\n## Sets up the necessary runtime container dependencies for Komodo Core.\n## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.\n\nARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest\nARG FRONTEND_IMAGE=ghcr.io/moghtech/komodo-frontend:latest\nARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64\nARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64\n\n# This is required to work with COPY --from\nFROM ${X86_64_BINARIES} AS x86_64\nFROM ${AARCH64_BINARIES} AS aarch64\nFROM ${FRONTEND_IMAGE} AS frontend\n\n# Final Image\nFROM debian:bullseye-slim\n\nCOPY ./bin/core/starship.toml /starship.toml\nCOPY ./bin/core/debian-deps.sh .\nRUN sh ./debian-deps.sh && rm ./debian-deps.sh\n\nWORKDIR /app\n\nARG TARGETPLATFORM\n\n# Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.\nCOPY --from=x86_64 /core /app/core/linux/amd64\nCOPY --from=aarch64 /core /app/core/linux/arm64\nRUN mv /app/core/${TARGETPLATFORM} /usr/local/bin/core && rm -r /app/core\n\n# Same for util\nCOPY --from=x86_64 /km /app/km/linux/amd64\nCOPY --from=aarch64 /km /app/km/linux/arm64\nRUN mv /app/km/${TARGETPLATFORM} /usr/local/bin/km && rm -r /app/km\n\n# Copy default config / static frontend / deno binary\nCOPY ./config/core.config.toml /config/.default.config.toml\nCOPY --from=frontend /frontend /app/frontend\nCOPY --from=denoland/deno:bin /deno /usr/local/bin/deno\n\n# Set $DENO_DIR and preload external Deno deps\nENV DENO_DIR=/action-cache/deno\nRUN mkdir /action-cache && \\\n  cd /action-cache && \\\n  deno install jsr:@std/yaml jsr:@std/toml\n\n# Hint at the port\nEXPOSE 9120\n\nENV KOMODO_CLI_CONFIG_PATHS=\"/config\"\n# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`\nENV KOMODO_CLI_CONFIG_KEYWORDS=\"*config.*,*komodo.cli*.*\"\n\nCMD [ \"core\" ]\n\n# Label for Ghcr\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo Core\"\nLABEL org.opencontainers.image.licenses=GPL-3.0\n"
  },
  {
    "path": "bin/core/single-arch.Dockerfile",
    "content": "## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).\n## Sets up the necessary runtime container dependencies for Komodo Core.\n\nARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest\n\n# This is required to work with COPY --from\nFROM ${BINARIES_IMAGE} AS binaries\n\n# Build Frontend\nFROM node:20.12-alpine AS frontend-builder\nWORKDIR /builder\nCOPY ./frontend ./frontend\nCOPY ./client/core/ts ./client\nRUN cd client && yarn && yarn build && yarn link\nRUN cd frontend && yarn link komodo_client && yarn && yarn build\n\nFROM debian:bullseye-slim\n\nCOPY ./bin/core/starship.toml /starship.toml\nCOPY ./bin/core/debian-deps.sh .\nRUN sh ./debian-deps.sh && rm ./debian-deps.sh\n\t\n# Copy\nCOPY ./config/core.config.toml /config/.default.config.toml\nCOPY --from=frontend-builder /builder/frontend/dist /app/frontend\nCOPY --from=binaries /core /usr/local/bin/core\nCOPY --from=binaries /km /usr/local/bin/km\nCOPY --from=denoland/deno:bin /deno /usr/local/bin/deno\n\n# Set $DENO_DIR and preload external Deno deps\nENV DENO_DIR=/action-cache/deno\nRUN mkdir /action-cache && \\\n\tcd /action-cache && \\\n\tdeno install jsr:@std/yaml jsr:@std/toml\n\n# Hint at the port\nEXPOSE 9120\n\nENV KOMODO_CLI_CONFIG_PATHS=\"/config\"\n# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`\nENV KOMODO_CLI_CONFIG_KEYWORDS=\"*config.*,*komodo.cli*.*\"\n\nCMD [ \"core\" ]\n\n# Label for Ghcr\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo Core\"\nLABEL org.opencontainers.image.licenses=GPL-3.0\n"
  },
  {
    "path": "bin/core/src/alert/discord.rs",
    "content": "use std::sync::OnceLock;\n\nuse serde::Serialize;\n\nuse super::*;\n\n#[instrument(level = \"debug\")]\npub async fn send_alert(\n  url: &str,\n  alert: &Alert,\n) -> anyhow::Result<()> {\n  let level = fmt_level(alert.level);\n  let content = match &alert.data {\n    AlertData::Test { id, name } => {\n      let link = resource_link(ResourceTargetVariant::Alerter, id);\n      format!(\n        \"{level} | If you see this message, then Alerter **{name}** is **working**\\n{link}\"\n      )\n    }\n    AlertData::ServerVersionMismatch {\n      id,\n      name,\n      region,\n      server_version,\n      core_version,\n    } => {\n      let region = fmt_region(region);\n      let link = resource_link(ResourceTargetVariant::Server, id);\n      match alert.level {\n        SeverityLevel::Ok => {\n          format!(\n            \"{level} | **{name}**{region} | Periphery version now matches Core version ✅\\n{link}\"\n          )\n        }\n        _ => {\n          format!(\n            \"{level} | **{name}**{region} | Version mismatch detected ⚠️\\nPeriphery: **{server_version}** | Core: **{core_version}**\\n{link}\"\n          )\n        }\n      }\n    }\n    AlertData::ServerUnreachable {\n      id,\n      name,\n      region,\n      err,\n    } => {\n      let region = fmt_region(region);\n      let link = resource_link(ResourceTargetVariant::Server, id);\n      match alert.level {\n        SeverityLevel::Ok => {\n          format!(\n            \"{level} | **{name}**{region} is now **reachable**\\n{link}\"\n          )\n        }\n        SeverityLevel::Critical => {\n          let err = err\n            .as_ref()\n            .map(|e| format!(\"\\n**error**: {e:#?}\"))\n            .unwrap_or_default();\n          format!(\n            \"{level} | **{name}**{region} is **unreachable** ❌\\n{link}{err}\"\n          )\n        }\n        _ => unreachable!(),\n      }\n    }\n    AlertData::ServerCpu {\n      id,\n      name,\n      region,\n      percentage,\n    } => {\n      let region = fmt_region(region);\n      let link = resource_link(ResourceTargetVariant::Server, id);\n      format!(\n        \"{level} | **{name}**{region} cpu usage at **{percentage:.1}%**\\n{link}\"\n      )\n    }\n    AlertData::ServerMem {\n      id,\n      name,\n      region,\n      used_gb,\n      total_gb,\n    } => {\n      let region = fmt_region(region);\n      let link = resource_link(ResourceTargetVariant::Server, id);\n      let percentage = 100.0 * used_gb / total_gb;\n      format!(\n        \"{level} | **{name}**{region} memory usage at **{percentage:.1}%** 💾\\n\\nUsing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\\n{link}\"\n      )\n    }\n    AlertData::ServerDisk {\n      id,\n      name,\n      region,\n      path,\n      used_gb,\n      total_gb,\n    } => {\n      let region = fmt_region(region);\n      let link = resource_link(ResourceTargetVariant::Server, id);\n      let percentage = 100.0 * used_gb / total_gb;\n      format!(\n        \"{level} | **{name}**{region} disk usage at **{percentage:.1}%** 💿\\nmount point: `{path:?}`\\nusing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\\n{link}\"\n      )\n    }\n    AlertData::ContainerStateChange {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      from,\n      to,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Deployment, id);\n      let to = fmt_docker_container_state(to);\n      format!(\n        \"📦 Deployment **{name}** is now **{to}**\\nserver: **{server_name}**\\nprevious: **{from}**\\n{link}\"\n      )\n    }\n    AlertData::DeploymentImageUpdateAvailable {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      image,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Deployment, id);\n      format!(\n        \"⬆ Deployment **{name}** has an update available\\nserver: **{server_name}**\\nimage: **{image}**\\n{link}\"\n      )\n    }\n    AlertData::DeploymentAutoUpdated {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      image,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Deployment, id);\n      format!(\n        \"⬆ Deployment **{name}** was updated automatically ⏫\\nserver: **{server_name}**\\nimage: **{image}**\\n{link}\"\n      )\n    }\n    AlertData::StackStateChange {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      from,\n      to,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Stack, id);\n      let to = fmt_stack_state(to);\n      format!(\n        \"🥞 Stack **{name}** is now {to}\\nserver: **{server_name}**\\nprevious: **{from}**\\n{link}\"\n      )\n    }\n    AlertData::StackImageUpdateAvailable {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      service,\n      image,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Stack, id);\n      format!(\n        \"⬆ Stack **{name}** has an update available\\nserver: **{server_name}**\\nservice: **{service}**\\nimage: **{image}**\\n{link}\"\n      )\n    }\n    AlertData::StackAutoUpdated {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      images,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Stack, id);\n      let images_label =\n        if images.len() > 1 { \"images\" } else { \"image\" };\n      let images = images.join(\", \");\n      format!(\n        \"⬆ Stack **{name}** was updated automatically ⏫\\nserver: **{server_name}**\\n{images_label}: **{images}**\\n{link}\"\n      )\n    }\n    AlertData::AwsBuilderTerminationFailed {\n      instance_id,\n      message,\n    } => {\n      format!(\n        \"{level} | Failed to terminated AWS builder instance\\ninstance id: **{instance_id}**\\n{message}\"\n      )\n    }\n    AlertData::ResourceSyncPendingUpdates { id, name } => {\n      let link =\n        resource_link(ResourceTargetVariant::ResourceSync, id);\n      format!(\n        \"{level} | Pending resource sync updates on **{name}**\\n{link}\"\n      )\n    }\n    AlertData::BuildFailed { id, name, version } => {\n      let link = resource_link(ResourceTargetVariant::Build, id);\n      format!(\n        \"{level} | Build **{name}** failed\\nversion: **v{version}**\\n{link}\"\n      )\n    }\n    AlertData::RepoBuildFailed { id, name } => {\n      let link = resource_link(ResourceTargetVariant::Repo, id);\n      format!(\"{level} | Repo build for **{name}** failed\\n{link}\")\n    }\n    AlertData::ProcedureFailed { id, name } => {\n      let link = resource_link(ResourceTargetVariant::Procedure, id);\n      format!(\"{level} | Procedure **{name}** failed\\n{link}\")\n    }\n    AlertData::ActionFailed { id, name } => {\n      let link = resource_link(ResourceTargetVariant::Action, id);\n      format!(\"{level} | Action **{name}** failed\\n{link}\")\n    }\n    AlertData::ScheduleRun {\n      resource_type,\n      id,\n      name,\n    } => {\n      let link = resource_link(*resource_type, id);\n      format!(\n        \"{level} | **{name}** ({resource_type}) | Scheduled run started 🕝\\n{link}\"\n      )\n    }\n    AlertData::Custom { message, details } => {\n      format!(\n        \"{level} | {message}{}\",\n        if details.is_empty() {\n          format_args!(\"\")\n        } else {\n          format_args!(\"\\n{details}\")\n        }\n      )\n    }\n    AlertData::None {} => Default::default(),\n  };\n  if !content.is_empty() {\n    let VariablesAndSecrets { variables, secrets } =\n      get_variables_and_secrets().await?;\n    let mut url_interpolated = url.to_string();\n\n    let mut interpolator =\n      Interpolator::new(Some(&variables), &secrets);\n\n    interpolator.interpolate_string(&mut url_interpolated)?;\n\n    send_message(&url_interpolated, &content)\n      .await\n      .map_err(|e| {\n        let replacers = interpolator\n          .secret_replacers\n          .into_iter()\n          .collect::<Vec<_>>();\n        let sanitized_error =\n          svi::replace_in_string(&format!(\"{e:?}\"), &replacers);\n        anyhow::Error::msg(format!(\n          \"Error with slack request: {sanitized_error}\"\n        ))\n      })?;\n  }\n  Ok(())\n}\n\nasync fn send_message(\n  url: &str,\n  content: &str,\n) -> anyhow::Result<()> {\n  let body = DiscordMessageBody { content };\n\n  let response = http_client()\n    .post(url)\n    .json(&body)\n    .send()\n    .await\n    .context(\"Failed to send message\")?;\n\n  let status = response.status();\n\n  if status.is_success() {\n    Ok(())\n  } else {\n    let text = response.text().await.with_context(|| {\n      format!(\"Failed to send message to Discord | {status} | failed to get response text\")\n    })?;\n    Err(anyhow::anyhow!(\n      \"Failed to send message to Discord | {status} | {text}\"\n    ))\n  }\n}\n\nfn http_client() -> &'static reqwest::Client {\n  static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();\n  CLIENT.get_or_init(reqwest::Client::new)\n}\n\n#[derive(Serialize)]\nstruct DiscordMessageBody<'a> {\n  content: &'a str,\n}\n"
  },
  {
    "path": "bin/core/src/alert/mod.rs",
    "content": "use ::slack::types::Block;\nuse anyhow::{Context, anyhow};\nuse database::mungos::{find::find_collect, mongodb::bson::doc};\nuse derive_variants::ExtractVariant;\nuse futures::future::join_all;\nuse interpolate::Interpolator;\nuse komodo_client::entities::{\n  ResourceTargetVariant,\n  alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},\n  alerter::*,\n  deployment::DeploymentState,\n  komodo_timestamp,\n  stack::StackState,\n};\nuse tracing::Instrument;\n\nuse crate::helpers::query::get_variables_and_secrets;\nuse crate::helpers::{\n  maintenance::is_in_maintenance, query::VariablesAndSecrets,\n};\nuse crate::{config::core_config, state::db_client};\n\nmod discord;\nmod ntfy;\nmod pushover;\nmod slack;\n\n#[instrument(level = \"debug\")]\npub async fn send_alerts(alerts: &[Alert]) {\n  if alerts.is_empty() {\n    return;\n  }\n\n  let span =\n    info_span!(\"send_alerts\", alerts = format!(\"{alerts:?}\"));\n  async {\n    let Ok(alerters) = find_collect(\n      &db_client().alerters,\n      doc! { \"config.enabled\": true },\n      None,\n    )\n    .await\n    .inspect_err(|e| {\n      error!(\n      \"ERROR sending alerts | failed to get alerters from db | {e:#}\"\n    )\n    }) else {\n      return;\n    };\n\n    let handles = alerts\n      .iter()\n      .map(|alert| send_alert_to_alerters(&alerters, alert));\n\n    join_all(handles).await;\n  }\n  .instrument(span)\n  .await\n}\n\n#[instrument(level = \"debug\")]\nasync fn send_alert_to_alerters(alerters: &[Alerter], alert: &Alert) {\n  if alerters.is_empty() {\n    return;\n  }\n\n  let handles = alerters\n    .iter()\n    .map(|alerter| send_alert_to_alerter(alerter, alert));\n\n  join_all(handles)\n    .await\n    .into_iter()\n    .filter_map(|res| res.err())\n    .for_each(|e| error!(\"{e:#}\"));\n}\n\npub async fn send_alert_to_alerter(\n  alerter: &Alerter,\n  alert: &Alert,\n) -> anyhow::Result<()> {\n  // Don't send if not enabled\n  if !alerter.config.enabled {\n    return Ok(());\n  }\n\n  if is_in_maintenance(\n    &alerter.config.maintenance_windows,\n    komodo_timestamp(),\n  ) {\n    return Ok(());\n  }\n\n  let alert_type = alert.data.extract_variant();\n\n  // In the test case, we don't want the filters inside this\n  // block to stop the test from being sent to the alerting endpoint.\n  if alert_type != AlertDataVariant::Test {\n    // Don't send if alert type not configured on the alerter\n    if !alerter.config.alert_types.is_empty()\n      && !alerter.config.alert_types.contains(&alert_type)\n    {\n      return Ok(());\n    }\n\n    // Don't send if resource is in the blacklist\n    if alerter.config.except_resources.contains(&alert.target) {\n      return Ok(());\n    }\n\n    // Don't send if whitelist configured and target is not included\n    if !alerter.config.resources.is_empty()\n      && !alerter.config.resources.contains(&alert.target)\n    {\n      return Ok(());\n    }\n  }\n\n  match &alerter.config.endpoint {\n    AlerterEndpoint::Custom(CustomAlerterEndpoint { url }) => {\n      send_custom_alert(url, alert).await.with_context(|| {\n        format!(\n          \"Failed to send alert to Custom Alerter {}\",\n          alerter.name\n        )\n      })\n    }\n    AlerterEndpoint::Slack(SlackAlerterEndpoint { url }) => {\n      slack::send_alert(url, alert).await.with_context(|| {\n        format!(\n          \"Failed to send alert to Slack Alerter {}\",\n          alerter.name\n        )\n      })\n    }\n    AlerterEndpoint::Discord(DiscordAlerterEndpoint { url }) => {\n      discord::send_alert(url, alert).await.with_context(|| {\n        format!(\n          \"Failed to send alert to Discord Alerter {}\",\n          alerter.name\n        )\n      })\n    }\n    AlerterEndpoint::Ntfy(NtfyAlerterEndpoint { url, email }) => {\n      ntfy::send_alert(url, email.as_deref(), alert)\n        .await\n        .with_context(|| {\n          format!(\n            \"Failed to send alert to ntfy Alerter {}\",\n            alerter.name\n          )\n        })\n    }\n    AlerterEndpoint::Pushover(PushoverAlerterEndpoint { url }) => {\n      pushover::send_alert(url, alert).await.with_context(|| {\n        format!(\n          \"Failed to send alert to Pushover Alerter {}\",\n          alerter.name\n        )\n      })\n    }\n  }\n}\n\n#[instrument(level = \"debug\")]\nasync fn send_custom_alert(\n  url: &str,\n  alert: &Alert,\n) -> anyhow::Result<()> {\n  let VariablesAndSecrets { variables, secrets } =\n    get_variables_and_secrets().await?;\n  let mut url_interpolated = url.to_string();\n\n  let mut interpolator =\n    Interpolator::new(Some(&variables), &secrets);\n\n  interpolator.interpolate_string(&mut url_interpolated)?;\n\n  let res = reqwest::Client::new()\n    .post(url_interpolated)\n    .json(alert)\n    .send()\n    .await\n    .map_err(|e| {\n      let replacers = interpolator\n        .secret_replacers\n        .into_iter()\n        .collect::<Vec<_>>();\n      let sanitized_error =\n        svi::replace_in_string(&format!(\"{e:?}\"), &replacers);\n      anyhow::Error::msg(format!(\n        \"Error with request: {sanitized_error}\"\n      ))\n    })\n    .context(\"failed at post request to alerter\")?;\n  let status = res.status();\n  if !status.is_success() {\n    let text = res\n      .text()\n      .await\n      .context(\"failed to get response text on alerter response\")?;\n    return Err(anyhow!(\n      \"post to alerter failed | {status} | {text}\"\n    ));\n  }\n  Ok(())\n}\n\nfn fmt_region(region: &Option<String>) -> String {\n  match region {\n    Some(region) => format!(\" ({region})\"),\n    None => String::new(),\n  }\n}\n\nfn fmt_docker_container_state(state: &DeploymentState) -> String {\n  match state {\n    DeploymentState::Running => String::from(\"Running ▶️\"),\n    DeploymentState::Exited => String::from(\"Exited 🛑\"),\n    DeploymentState::Restarting => String::from(\"Restarting 🔄\"),\n    DeploymentState::NotDeployed => String::from(\"Not Deployed\"),\n    _ => state.to_string(),\n  }\n}\n\nfn fmt_stack_state(state: &StackState) -> String {\n  match state {\n    StackState::Running => String::from(\"Running ▶️\"),\n    StackState::Stopped => String::from(\"Stopped 🛑\"),\n    StackState::Restarting => String::from(\"Restarting 🔄\"),\n    StackState::Down => String::from(\"Down ⬇️\"),\n    _ => state.to_string(),\n  }\n}\n\nfn fmt_level(level: SeverityLevel) -> &'static str {\n  match level {\n    SeverityLevel::Critical => \"CRITICAL 🚨\",\n    SeverityLevel::Warning => \"WARNING ‼️\",\n    SeverityLevel::Ok => \"OK ✅\",\n  }\n}\n\nfn resource_link(\n  resource_type: ResourceTargetVariant,\n  id: &str,\n) -> String {\n  komodo_client::entities::resource_link(\n    &core_config().host,\n    resource_type,\n    id,\n  )\n}\n\n/// Standard message content format\n/// used by Ntfy, Pushover.\nfn standard_alert_content(alert: &Alert) -> String {\n  let level = fmt_level(alert.level);\n  match &alert.data {\n    AlertData::Test { id, name } => {\n      let link = resource_link(ResourceTargetVariant::Alerter, id);\n      format!(\n        \"{level} | If you see this message, then Alerter {name} is working\\n{link}\",\n      )\n    }\n    AlertData::ServerVersionMismatch {\n      id,\n      name,\n      region,\n      server_version,\n      core_version,\n    } => {\n      let region = fmt_region(region);\n      let link = resource_link(ResourceTargetVariant::Server, id);\n      match alert.level {\n        SeverityLevel::Ok => {\n          format!(\n            \"{level} | {name}{region} | Periphery version now matches Core version ✅\\n{link}\"\n          )\n        }\n        _ => {\n          format!(\n            \"{level} | {name}{region} | Version mismatch detected ⚠️\\nPeriphery: {server_version} | Core: {core_version}\\n{link}\"\n          )\n        }\n      }\n    }\n    AlertData::ServerUnreachable {\n      id,\n      name,\n      region,\n      err,\n    } => {\n      let region = fmt_region(region);\n      let link = resource_link(ResourceTargetVariant::Server, id);\n      match alert.level {\n        SeverityLevel::Ok => {\n          format!(\"{level} | {name}{region} is now reachable\\n{link}\")\n        }\n        SeverityLevel::Critical => {\n          let err = err\n            .as_ref()\n            .map(|e| format!(\"\\nerror: {e:#?}\"))\n            .unwrap_or_default();\n          format!(\n            \"{level} | {name}{region} is unreachable ❌\\n{link}{err}\"\n          )\n        }\n        _ => unreachable!(),\n      }\n    }\n    AlertData::ServerCpu {\n      id,\n      name,\n      region,\n      percentage,\n    } => {\n      let region = fmt_region(region);\n      let link = resource_link(ResourceTargetVariant::Server, id);\n      format!(\n        \"{level} | {name}{region} cpu usage at {percentage:.1}%\\n{link}\",\n      )\n    }\n    AlertData::ServerMem {\n      id,\n      name,\n      region,\n      used_gb,\n      total_gb,\n    } => {\n      let region = fmt_region(region);\n      let link = resource_link(ResourceTargetVariant::Server, id);\n      let percentage = 100.0 * used_gb / total_gb;\n      format!(\n        \"{level} | {name}{region} memory usage at {percentage:.1}%💾\\n\\nUsing {used_gb:.1} GiB / {total_gb:.1} GiB\\n{link}\",\n      )\n    }\n    AlertData::ServerDisk {\n      id,\n      name,\n      region,\n      path,\n      used_gb,\n      total_gb,\n    } => {\n      let region = fmt_region(region);\n      let link = resource_link(ResourceTargetVariant::Server, id);\n      let percentage = 100.0 * used_gb / total_gb;\n      format!(\n        \"{level} | {name}{region} disk usage at {percentage:.1}%💿\\nmount point: {path:?}\\nusing {used_gb:.1} GiB / {total_gb:.1} GiB\\n{link}\",\n      )\n    }\n    AlertData::ContainerStateChange {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      from,\n      to,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Deployment, id);\n      let to_state = fmt_docker_container_state(to);\n      format!(\n        \"📦Deployment {name} is now {to_state}\\nserver: {server_name}\\nprevious: {from}\\n{link}\",\n      )\n    }\n    AlertData::DeploymentImageUpdateAvailable {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      image,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Deployment, id);\n      format!(\n        \"⬆ Deployment {name} has an update available\\nserver: {server_name}\\nimage: {image}\\n{link}\",\n      )\n    }\n    AlertData::DeploymentAutoUpdated {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      image,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Deployment, id);\n      format!(\n        \"⬆ Deployment {name} was updated automatically\\nserver: {server_name}\\nimage: {image}\\n{link}\",\n      )\n    }\n    AlertData::StackStateChange {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      from,\n      to,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Stack, id);\n      let to_state = fmt_stack_state(to);\n      format!(\n        \"🥞 Stack {name} is now {to_state}\\nserver: {server_name}\\nprevious: {from}\\n{link}\",\n      )\n    }\n    AlertData::StackImageUpdateAvailable {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      service,\n      image,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Stack, id);\n      format!(\n        \"⬆ Stack {name} has an update available\\nserver: {server_name}\\nservice: {service}\\nimage: {image}\\n{link}\",\n      )\n    }\n    AlertData::StackAutoUpdated {\n      id,\n      name,\n      server_id: _server_id,\n      server_name,\n      images,\n    } => {\n      let link = resource_link(ResourceTargetVariant::Stack, id);\n      let images_label =\n        if images.len() > 1 { \"images\" } else { \"image\" };\n      let images_str = images.join(\", \");\n      format!(\n        \"⬆ Stack {name} was updated automatically ⏫\\nserver: {server_name}\\n{images_label}: {images_str}\\n{link}\",\n      )\n    }\n    AlertData::AwsBuilderTerminationFailed {\n      instance_id,\n      message,\n    } => {\n      format!(\n        \"{level} | Failed to terminate AWS builder instance\\ninstance id: {instance_id}\\n{message}\",\n      )\n    }\n    AlertData::ResourceSyncPendingUpdates { id, name } => {\n      let link =\n        resource_link(ResourceTargetVariant::ResourceSync, id);\n      format!(\n        \"{level} | Pending resource sync updates on {name}\\n{link}\",\n      )\n    }\n    AlertData::BuildFailed { id, name, version } => {\n      let link = resource_link(ResourceTargetVariant::Build, id);\n      format!(\n        \"{level} | Build {name} failed\\nversion: v{version}\\n{link}\",\n      )\n    }\n    AlertData::RepoBuildFailed { id, name } => {\n      let link = resource_link(ResourceTargetVariant::Repo, id);\n      format!(\"{level} | Repo build for {name} failed\\n{link}\",)\n    }\n    AlertData::ProcedureFailed { id, name } => {\n      let link = resource_link(ResourceTargetVariant::Procedure, id);\n      format!(\"{level} | Procedure {name} failed\\n{link}\")\n    }\n    AlertData::ActionFailed { id, name } => {\n      let link = resource_link(ResourceTargetVariant::Action, id);\n      format!(\"{level} | Action {name} failed\\n{link}\")\n    }\n    AlertData::ScheduleRun {\n      resource_type,\n      id,\n      name,\n    } => {\n      let link = resource_link(*resource_type, id);\n      format!(\n        \"{level} | {name} ({resource_type}) | Scheduled run started 🕝\\n{link}\"\n      )\n    }\n    AlertData::Custom { message, details } => {\n      format!(\n        \"{level} | {message}{}\",\n        if details.is_empty() {\n          format_args!(\"\")\n        } else {\n          format_args!(\"\\n{details}\")\n        }\n      )\n    }\n    AlertData::None {} => Default::default(),\n  }\n}\n"
  },
  {
    "path": "bin/core/src/alert/ntfy.rs",
    "content": "use std::sync::OnceLock;\n\nuse super::*;\n\n#[instrument(level = \"debug\")]\npub async fn send_alert(\n  url: &str,\n  email: Option<&str>,\n  alert: &Alert,\n) -> anyhow::Result<()> {\n  let content = standard_alert_content(alert);\n  if !content.is_empty() {\n    send_message(url, email, content).await?;\n  }\n  Ok(())\n}\n\nasync fn send_message(\n  url: &str,\n  email: Option<&str>,\n  content: String,\n) -> anyhow::Result<()> {\n  let mut request = http_client()\n    .post(url)\n    .header(\"Title\", \"ntfy Alert\")\n    .body(content);\n\n  if let Some(email) = email {\n    request = request.header(\"X-Email\", email);\n  }\n\n  let response =\n    request.send().await.context(\"Failed to send message\")?;\n\n  let status = response.status();\n  if status.is_success() {\n    debug!(\"ntfy alert sent successfully: {}\", status);\n    Ok(())\n  } else {\n    let text = response.text().await.with_context(|| {\n      format!(\n        \"Failed to send message to ntfy | {status} | failed to get response text\"\n      )\n    })?;\n    Err(anyhow!(\n      \"Failed to send message to ntfy | {} | {}\",\n      status,\n      text\n    ))\n  }\n}\n\nfn http_client() -> &'static reqwest::Client {\n  static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();\n  CLIENT.get_or_init(reqwest::Client::new)\n}\n"
  },
  {
    "path": "bin/core/src/alert/pushover.rs",
    "content": "use std::sync::OnceLock;\n\nuse super::*;\n\n#[instrument(level = \"debug\")]\npub async fn send_alert(\n  url: &str,\n  alert: &Alert,\n) -> anyhow::Result<()> {\n  let content = standard_alert_content(alert);\n  if !content.is_empty() {\n    send_message(url, content).await?;\n  }\n  Ok(())\n}\n\nasync fn send_message(\n  url: &str,\n  content: String,\n) -> anyhow::Result<()> {\n  // pushover needs all information to be encoded in the URL. At minimum they need\n  // the user key, the application token, and the message (url encoded).\n  // other optional params here: https://pushover.net/api (just add them to the\n  // webhook url along with the application token and the user key).\n  let content = [(\"message\", content)];\n\n  let response = http_client()\n    .post(url)\n    .form(&content)\n    .send()\n    .await\n    .context(\"Failed to send message\")?;\n\n  let status = response.status();\n  if status.is_success() {\n    debug!(\"pushover alert sent successfully: {}\", status);\n    Ok(())\n  } else {\n    let text = response.text().await.with_context(|| {\n      format!(\n        \"Failed to send message to pushover | {status} | failed to get response text\"\n      )\n    })?;\n    Err(anyhow!(\n      \"Failed to send message to pushover | {} | {}\",\n      status,\n      text\n    ))\n  }\n}\n\nfn http_client() -> &'static reqwest::Client {\n  static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();\n  CLIENT.get_or_init(reqwest::Client::new)\n}\n"
  },
  {
    "path": "bin/core/src/alert/slack.rs",
    "content": "use super::*;\n\n#[instrument(level = \"debug\")]\npub async fn send_alert(\n  url: &str,\n  alert: &Alert,\n) -> anyhow::Result<()> {\n  let level = fmt_level(alert.level);\n  let (text, blocks): (_, Option<_>) = match &alert.data {\n    AlertData::Test { id, name } => {\n      let text = format!(\n        \"{level} | If you see this message, then Alerter *{name}* is *working*\"\n      );\n      let blocks = vec![\n        Block::header(level),\n        Block::section(format!(\n          \"If you see this message, then Alerter *{name}* is *working*\"\n        )),\n        Block::section(resource_link(\n          ResourceTargetVariant::Alerter,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::ServerVersionMismatch {\n      id,\n      name,\n      region,\n      server_version,\n      core_version,\n    } => {\n      let region = fmt_region(region);\n      let text = match alert.level {\n        SeverityLevel::Ok => {\n          format!(\n            \"{level} | *{name}*{region} | Periphery version now matches Core version ✅\"\n          )\n        }\n        _ => {\n          format!(\n            \"{level} | *{name}*{region} | Version mismatch detected ⚠️\\nPeriphery: {server_version} | Core: {core_version}\"\n          )\n        }\n      };\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(resource_link(\n          ResourceTargetVariant::Server,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::ServerUnreachable {\n      id,\n      name,\n      region,\n      err,\n    } => {\n      let region = fmt_region(region);\n      match alert.level {\n        SeverityLevel::Ok => {\n          let text =\n            format!(\"{level} | *{name}*{region} is now *reachable*\");\n          let blocks = vec![\n            Block::header(level),\n            Block::section(format!(\n              \"*{name}*{region} is now *reachable*\"\n            )),\n          ];\n          (text, blocks.into())\n        }\n        SeverityLevel::Critical => {\n          let text =\n            format!(\"{level} | *{name}*{region} is *unreachable* ❌\");\n          let err = err\n            .as_ref()\n            .map(|e| format!(\"\\nerror: {e:#?}\"))\n            .unwrap_or_default();\n          let blocks = vec![\n            Block::header(level),\n            Block::section(format!(\n              \"*{name}*{region} is *unreachable* ❌{err}\"\n            )),\n            Block::section(resource_link(\n              ResourceTargetVariant::Server,\n              id,\n            )),\n          ];\n          (text, blocks.into())\n        }\n        _ => unreachable!(),\n      }\n    }\n    AlertData::ServerCpu {\n      id,\n      name,\n      region,\n      percentage,\n    } => {\n      let region = fmt_region(region);\n      match alert.level {\n        SeverityLevel::Ok => {\n          let text = format!(\n            \"{level} | *{name}*{region} cpu usage at *{percentage:.1}%*\"\n          );\n          let blocks = vec![\n            Block::header(level),\n            Block::section(format!(\n              \"*{name}*{region} cpu usage at *{percentage:.1}%*\"\n            )),\n            Block::section(resource_link(\n              ResourceTargetVariant::Server,\n              id,\n            )),\n          ];\n          (text, blocks.into())\n        }\n        _ => {\n          let text = format!(\n            \"{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈\"\n          );\n          let blocks = vec![\n            Block::header(level),\n            Block::section(format!(\n              \"*{name}*{region} cpu usage at *{percentage:.1}%* 📈\"\n            )),\n            Block::section(resource_link(\n              ResourceTargetVariant::Server,\n              id,\n            )),\n          ];\n          (text, blocks.into())\n        }\n      }\n    }\n    AlertData::ServerMem {\n      id,\n      name,\n      region,\n      used_gb,\n      total_gb,\n    } => {\n      let region = fmt_region(region);\n      let percentage = 100.0 * used_gb / total_gb;\n      match alert.level {\n        SeverityLevel::Ok => {\n          let text = format!(\n            \"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾\"\n          );\n          let blocks = vec![\n            Block::header(level),\n            Block::section(format!(\n              \"*{name}*{region} memory usage at *{percentage:.1}%* 💾\"\n            )),\n            Block::section(format!(\n              \"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\"\n            )),\n            Block::section(resource_link(\n              ResourceTargetVariant::Server,\n              id,\n            )),\n          ];\n          (text, blocks.into())\n        }\n        _ => {\n          let text = format!(\n            \"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾\"\n          );\n          let blocks = vec![\n            Block::header(level),\n            Block::section(format!(\n              \"*{name}*{region} memory usage at *{percentage:.1}%* 💾\"\n            )),\n            Block::section(format!(\n              \"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\"\n            )),\n            Block::section(resource_link(\n              ResourceTargetVariant::Server,\n              id,\n            )),\n          ];\n          (text, blocks.into())\n        }\n      }\n    }\n    AlertData::ServerDisk {\n      id,\n      name,\n      region,\n      path,\n      used_gb,\n      total_gb,\n    } => {\n      let region = fmt_region(region);\n      let percentage = 100.0 * used_gb / total_gb;\n      match alert.level {\n        SeverityLevel::Ok => {\n          let text = format!(\n            \"{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿\"\n          );\n          let blocks = vec![\n            Block::header(level),\n            Block::section(format!(\n              \"*{name}*{region} disk usage at *{percentage:.1}%* 💿\"\n            )),\n            Block::section(format!(\n              \"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\"\n            )),\n            Block::section(resource_link(\n              ResourceTargetVariant::Server,\n              id,\n            )),\n          ];\n          (text, blocks.into())\n        }\n        _ => {\n          let text = format!(\n            \"{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿\"\n          );\n          let blocks = vec![\n            Block::header(level),\n            Block::section(format!(\n              \"*{name}*{region} disk usage at *{percentage:.1}%* 💿\"\n            )),\n            Block::section(format!(\n              \"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\"\n            )),\n            Block::section(resource_link(\n              ResourceTargetVariant::Server,\n              id,\n            )),\n          ];\n          (text, blocks.into())\n        }\n      }\n    }\n    AlertData::ContainerStateChange {\n      name,\n      server_name,\n      from,\n      to,\n      id,\n      ..\n    } => {\n      let to = fmt_docker_container_state(to);\n      let text = format!(\"📦 Container *{name}* is now *{to}*\");\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(format!(\n          \"server: {server_name}\\nprevious: {from}\",\n        )),\n        Block::section(resource_link(\n          ResourceTargetVariant::Deployment,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::DeploymentImageUpdateAvailable {\n      id,\n      name,\n      server_name,\n      server_id: _server_id,\n      image,\n    } => {\n      let text =\n        format!(\"⬆ Deployment *{name}* has an update available\");\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(format!(\n          \"server: *{server_name}*\\nimage: *{image}*\",\n        )),\n        Block::section(resource_link(\n          ResourceTargetVariant::Deployment,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::DeploymentAutoUpdated {\n      id,\n      name,\n      server_name,\n      server_id: _server_id,\n      image,\n    } => {\n      let text =\n        format!(\"⬆ Deployment *{name}* was updated automatically ⏫\");\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(format!(\n          \"server: *{server_name}*\\nimage: *{image}*\",\n        )),\n        Block::section(resource_link(\n          ResourceTargetVariant::Deployment,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::StackStateChange {\n      name,\n      server_name,\n      from,\n      to,\n      id,\n      ..\n    } => {\n      let to = fmt_stack_state(to);\n      let text = format!(\"🥞 Stack *{name}* is now *{to}*\");\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(format!(\n          \"server: *{server_name}*\\nprevious: *{from}*\",\n        )),\n        Block::section(resource_link(\n          ResourceTargetVariant::Stack,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::StackImageUpdateAvailable {\n      id,\n      name,\n      server_name,\n      server_id: _server_id,\n      service,\n      image,\n    } => {\n      let text = format!(\"⬆ Stack *{name}* has an update available\");\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(format!(\n          \"server: *{server_name}*\\nservice: *{service}*\\nimage: *{image}*\",\n        )),\n        Block::section(resource_link(\n          ResourceTargetVariant::Stack,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::StackAutoUpdated {\n      id,\n      name,\n      server_name,\n      server_id: _server_id,\n      images,\n    } => {\n      let text =\n        format!(\"⬆ Stack *{name}* was updated automatically ⏫\");\n      let images_label =\n        if images.len() > 1 { \"images\" } else { \"image\" };\n      let images = images.join(\", \");\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(format!(\n          \"server: *{server_name}*\\n{images_label}: *{images}*\",\n        )),\n        Block::section(resource_link(\n          ResourceTargetVariant::Stack,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::AwsBuilderTerminationFailed {\n      instance_id,\n      message,\n    } => {\n      let text = format!(\n        \"{level} | Failed to terminated AWS builder instance \"\n      );\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(format!(\n          \"instance id: *{instance_id}*\\n{message}\"\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::ResourceSyncPendingUpdates { id, name } => {\n      let text = format!(\n        \"{level} | Pending resource sync updates on *{name}*\"\n      );\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(format!(\n          \"sync id: *{id}*\\nsync name: *{name}*\",\n        )),\n        Block::section(resource_link(\n          ResourceTargetVariant::ResourceSync,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::BuildFailed { id, name, version } => {\n      let text = format!(\"{level} | Build {name} has failed\");\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(format!(\"version: *v{version}*\",)),\n        Block::section(resource_link(\n          ResourceTargetVariant::Build,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::RepoBuildFailed { id, name } => {\n      let text =\n        format!(\"{level} | Repo build for *{name}* has *failed*\");\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(resource_link(\n          ResourceTargetVariant::Repo,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::ProcedureFailed { id, name } => {\n      let text = format!(\"{level} | Procedure *{name}* has *failed*\");\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(resource_link(\n          ResourceTargetVariant::Procedure,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::ActionFailed { id, name } => {\n      let text = format!(\"{level} | Action *{name}* has *failed*\");\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(resource_link(\n          ResourceTargetVariant::Action,\n          id,\n        )),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::ScheduleRun {\n      resource_type,\n      id,\n      name,\n    } => {\n      let text = format!(\n        \"{level} | *{name}* ({resource_type}) | Scheduled run started 🕝\"\n      );\n      let blocks = vec![\n        Block::header(text.clone()),\n        Block::section(resource_link(*resource_type, id)),\n      ];\n      (text, blocks.into())\n    }\n    AlertData::Custom { message, details } => {\n      let text = format!(\"{level} | {message}\");\n      let blocks =\n        vec![Block::header(text.clone()), Block::section(details)];\n      (text, blocks.into())\n    }\n    AlertData::None {} => Default::default(),\n  };\n  if !text.is_empty() {\n    let VariablesAndSecrets { variables, secrets } =\n      get_variables_and_secrets().await?;\n    let mut url_interpolated = url.to_string();\n\n    let mut interpolator =\n      Interpolator::new(Some(&variables), &secrets);\n\n    interpolator.interpolate_string(&mut url_interpolated)?;\n\n    let slack = ::slack::Client::new(url_interpolated);\n    slack.send_message(text, blocks).await.map_err(|e| {\n      let replacers = interpolator\n        .secret_replacers\n        .into_iter()\n        .collect::<Vec<_>>();\n      let sanitized_error =\n        svi::replace_in_string(&format!(\"{e:?}\"), &replacers);\n      anyhow::Error::msg(format!(\n        \"Error with slack request: {sanitized_error}\"\n      ))\n    })?;\n  }\n  Ok(())\n}\n"
  },
  {
    "path": "bin/core/src/api/auth.rs",
    "content": "use std::{sync::OnceLock, time::Instant};\n\nuse axum::{Router, extract::Path, http::HeaderMap, routing::post};\nuse derive_variants::{EnumVariants, ExtractVariant};\nuse komodo_client::{api::auth::*, entities::user::User};\nuse reqwest::StatusCode;\nuse resolver_api::Resolve;\nuse response::Response;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse serror::{AddStatusCode, Json};\nuse typeshare::typeshare;\nuse uuid::Uuid;\n\nuse crate::{\n  auth::{\n    get_user_id_from_headers,\n    github::{self, client::github_oauth_client},\n    google::{self, client::google_oauth_client},\n    oidc::{self, client::oidc_client},\n  },\n  config::core_config,\n  helpers::query::get_user,\n  state::jwt_client,\n};\n\nuse super::Variant;\n\n#[derive(Default)]\npub struct AuthArgs {\n  pub headers: HeaderMap,\n}\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,\n)]\n#[args(AuthArgs)]\n#[response(Response)]\n#[error(serror::Error)]\n#[variant_derive(Debug)]\n#[serde(tag = \"type\", content = \"params\")]\n#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]\npub enum AuthRequest {\n  GetLoginOptions(GetLoginOptions),\n  SignUpLocalUser(SignUpLocalUser),\n  LoginLocalUser(LoginLocalUser),\n  ExchangeForJwt(ExchangeForJwt),\n  GetUser(GetUser),\n}\n\npub fn router() -> Router {\n  let mut router = Router::new()\n    .route(\"/\", post(handler))\n    .route(\"/{variant}\", post(variant_handler));\n\n  if core_config().local_auth {\n    info!(\"🔑 Local Login Enabled\");\n  }\n\n  if github_oauth_client().is_some() {\n    info!(\"🔑 Github Login Enabled\");\n    router = router.nest(\"/github\", github::router())\n  }\n\n  if google_oauth_client().is_some() {\n    info!(\"🔑 Google Login Enabled\");\n    router = router.nest(\"/google\", google::router())\n  }\n\n  if core_config().oidc_enabled {\n    info!(\"🔑 OIDC Login Enabled\");\n    router = router.nest(\"/oidc\", oidc::router())\n  }\n\n  router\n}\n\nasync fn variant_handler(\n  headers: HeaderMap,\n  Path(Variant { variant }): Path<Variant>,\n  Json(params): Json<serde_json::Value>,\n) -> serror::Result<axum::response::Response> {\n  let req: AuthRequest = serde_json::from_value(json!({\n    \"type\": variant,\n    \"params\": params,\n  }))?;\n  handler(headers, Json(req)).await\n}\n\n#[instrument(name = \"AuthHandler\", level = \"debug\", skip(headers))]\nasync fn handler(\n  headers: HeaderMap,\n  Json(request): Json<AuthRequest>,\n) -> serror::Result<axum::response::Response> {\n  let timer = Instant::now();\n  let req_id = Uuid::new_v4();\n  debug!(\n    \"/auth request {req_id} | METHOD: {:?}\",\n    request.extract_variant()\n  );\n  let res = request.resolve(&AuthArgs { headers }).await;\n  if let Err(e) = &res {\n    debug!(\"/auth request {req_id} | error: {:#}\", e.error);\n  }\n  let elapsed = timer.elapsed();\n  debug!(\"/auth request {req_id} | resolve time: {elapsed:?}\");\n  res.map(|res| res.0)\n}\n\nfn login_options_reponse() -> &'static GetLoginOptionsResponse {\n  static GET_LOGIN_OPTIONS_RESPONSE: OnceLock<\n    GetLoginOptionsResponse,\n  > = OnceLock::new();\n  GET_LOGIN_OPTIONS_RESPONSE.get_or_init(|| {\n    let config = core_config();\n    GetLoginOptionsResponse {\n      local: config.local_auth,\n      github: github_oauth_client().is_some(),\n      google: google_oauth_client().is_some(),\n      oidc: oidc_client().load().is_some(),\n      registration_disabled: config.disable_user_registration,\n    }\n  })\n}\n\nimpl Resolve<AuthArgs> for GetLoginOptions {\n  #[instrument(name = \"GetLoginOptions\", level = \"debug\", skip(self))]\n  async fn resolve(\n    self,\n    _: &AuthArgs,\n  ) -> serror::Result<GetLoginOptionsResponse> {\n    Ok(*login_options_reponse())\n  }\n}\n\nimpl Resolve<AuthArgs> for ExchangeForJwt {\n  #[instrument(name = \"ExchangeForJwt\", level = \"debug\", skip(self))]\n  async fn resolve(\n    self,\n    _: &AuthArgs,\n  ) -> serror::Result<ExchangeForJwtResponse> {\n    jwt_client()\n      .redeem_exchange_token(&self.token)\n      .await\n      .map_err(Into::into)\n  }\n}\n\nimpl Resolve<AuthArgs> for GetUser {\n  #[instrument(name = \"GetUser\", level = \"debug\", skip(self))]\n  async fn resolve(\n    self,\n    AuthArgs { headers }: &AuthArgs,\n  ) -> serror::Result<User> {\n    let user_id = get_user_id_from_headers(headers)\n      .await\n      .status_code(StatusCode::UNAUTHORIZED)?;\n    get_user(&user_id)\n      .await\n      .status_code(StatusCode::UNAUTHORIZED)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/action.rs",
    "content": "use std::{\n  collections::HashSet,\n  path::{Path, PathBuf},\n  str::FromStr,\n  sync::OnceLock,\n};\n\nuse anyhow::Context;\nuse command::run_komodo_command;\nuse config::merge_objects;\nuse database::mungos::{\n  by_id::update_one_by_id, mongodb::bson::to_document,\n};\nuse interpolate::Interpolator;\nuse komodo_client::{\n  api::{\n    execute::{BatchExecutionResponse, BatchRunAction, RunAction},\n    user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},\n  },\n  entities::{\n    FileFormat, JsonObject,\n    action::Action,\n    alert::{Alert, AlertData, SeverityLevel},\n    config::core::CoreConfig,\n    komodo_timestamp,\n    permission::PermissionLevel,\n    update::Update,\n    user::action_user,\n  },\n  parsers::parse_key_value_list,\n};\nuse resolver_api::Resolve;\nuse tokio::fs;\n\nuse crate::{\n  alert::send_alerts,\n  api::{execute::ExecuteRequest, user::UserArgs},\n  config::core_config,\n  helpers::{\n    query::{VariablesAndSecrets, get_variables_and_secrets},\n    random_string,\n    update::update_update,\n  },\n  permission::get_check_permissions,\n  resource::refresh_action_state_cache,\n  state::{action_states, db_client},\n};\n\nuse super::ExecuteArgs;\n\nimpl super::BatchExecute for BatchRunAction {\n  type Resource = Action;\n  fn single_request(action: String) -> ExecuteRequest {\n    ExecuteRequest::RunAction(RunAction {\n      action,\n      args: Default::default(),\n    })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchRunAction {\n  #[instrument(name = \"BatchRunAction\", skip(self, user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchRunAction>(&self.pattern, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<ExecuteArgs> for RunAction {\n  #[instrument(name = \"RunAction\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let mut action = get_check_permissions::<Action>(\n      &self.action,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the action (or insert default).\n    let action_state = action_states()\n      .action\n      .get_or_insert_default(&action.id)\n      .await;\n\n    // This will set action state back to default when dropped.\n    // Will also check to ensure action not already busy before updating.\n    let _action_guard = action_state.update_custom(\n      |state| state.running += 1,\n      |state| state.running -= 1,\n      false,\n    )?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let default_args = parse_action_arguments(\n      &action.config.arguments,\n      action.config.arguments_format,\n    )\n    .context(\"Failed to parse default Action arguments\")?;\n\n    let args = merge_objects(\n      default_args,\n      self.args.unwrap_or_default(),\n      true,\n      true,\n    )\n    .context(\"Failed to merge request args with default args\")?;\n\n    let args = serde_json::to_string(&args)\n      .context(\"Failed to serialize action run arguments\")?;\n\n    let CreateApiKeyResponse { key, secret } = CreateApiKey {\n      name: update.id.clone(),\n      expires: 0,\n    }\n    .resolve(&UserArgs {\n      user: action_user().to_owned(),\n    })\n    .await?;\n\n    let contents = &mut action.config.file_contents;\n\n    // Wrap the file contents in the execution context.\n    *contents = full_contents(contents, &args, &key, &secret);\n\n    let replacers =\n      interpolate(contents, &mut update, key.clone(), secret.clone())\n        .await?\n        .into_iter()\n        .collect::<Vec<_>>();\n\n    let file = format!(\"{}.ts\", random_string(10));\n    let path = core_config().action_directory.join(&file);\n\n    if let Some(parent) = path.parent() {\n      fs::create_dir_all(parent)\n        .await\n        .with_context(|| format!(\"Failed to initialize Action file parent directory {parent:?}\"))?;\n    }\n\n    fs::write(&path, contents).await.with_context(|| {\n      format!(\"Failed to write action file to {path:?}\")\n    })?;\n\n    let CoreConfig { ssl_enabled, .. } = core_config();\n\n    let https_cert_flag = if *ssl_enabled {\n      \" --unsafely-ignore-certificate-errors=localhost\"\n    } else {\n      \"\"\n    };\n\n    let reload = if action.config.reload_deno_deps {\n      \" --reload\"\n    } else {\n      \"\"\n    };\n\n    let mut res = run_komodo_command(\n      // Keep this stage name as is, the UI will find the latest update log by matching the stage name\n      \"Execute Action\",\n      None,\n      format!(\n        \"deno run --allow-all{https_cert_flag}{reload} {}\",\n        path.display()\n      ),\n    )\n    .await;\n\n    res.stdout = svi::replace_in_string(&res.stdout, &replacers)\n      .replace(&key, \"<ACTION_API_KEY>\");\n    res.stderr = svi::replace_in_string(&res.stderr, &replacers)\n      .replace(&secret, \"<ACTION_API_SECRET>\");\n\n    cleanup_run(file + \".js\", &path).await;\n\n    if let Err(e) = (DeleteApiKey { key })\n      .resolve(&UserArgs {\n        user: action_user().to_owned(),\n      })\n      .await\n    {\n      warn!(\n        \"Failed to delete API key after action execution | {:#}\",\n        e.error\n      );\n    };\n\n    update.logs.push(res);\n    update.finalize();\n\n    // Need to manually update the update before cache refresh,\n    // and before broadcast with update_update.\n    // The Err case of to_document should be unreachable,\n    // but will fail to update cache in that case.\n    if let Ok(update_doc) = to_document(&update) {\n      let _ = update_one_by_id(\n        &db_client().updates,\n        &update.id,\n        database::mungos::update::Update::Set(update_doc),\n        None,\n      )\n      .await;\n      refresh_action_state_cache().await;\n    }\n\n    update_update(update.clone()).await?;\n\n    if !update.success && action.config.failure_alert {\n      warn!(\"action unsuccessful, alerting...\");\n      let target = update.target.clone();\n      tokio::spawn(async move {\n        let alert = Alert {\n          id: Default::default(),\n          target,\n          ts: komodo_timestamp(),\n          resolved_ts: Some(komodo_timestamp()),\n          resolved: true,\n          level: SeverityLevel::Warning,\n          data: AlertData::ActionFailed {\n            id: action.id,\n            name: action.name,\n          },\n        };\n        send_alerts(&[alert]).await\n      });\n    }\n\n    Ok(update)\n  }\n}\n\nasync fn interpolate(\n  contents: &mut String,\n  update: &mut Update,\n  key: String,\n  secret: String,\n) -> serror::Result<HashSet<(String, String)>> {\n  let VariablesAndSecrets {\n    variables,\n    mut secrets,\n  } = get_variables_and_secrets().await?;\n\n  secrets.insert(String::from(\"ACTION_API_KEY\"), key);\n  secrets.insert(String::from(\"ACTION_API_SECRET\"), secret);\n\n  let mut interpolator =\n    Interpolator::new(Some(&variables), &secrets);\n\n  interpolator\n    .interpolate_string(contents)?\n    .push_logs(&mut update.logs);\n\n  Ok(interpolator.secret_replacers)\n}\n\nfn full_contents(\n  contents: &str,\n  // Pre-serialized to JSON string.\n  args: &str,\n  key: &str,\n  secret: &str,\n) -> String {\n  let CoreConfig {\n    port, ssl_enabled, ..\n  } = core_config();\n  let protocol = if *ssl_enabled { \"https\" } else { \"http\" };\n  let base_url = format!(\"{protocol}://localhost:{port}\");\n  format!(\n    \"import {{ KomodoClient, Types }} from '{base_url}/client/lib.js';\nimport * as __YAML__ from 'jsr:@std/yaml';\nimport * as __TOML__ from 'jsr:@std/toml';\n\nconst YAML = {{\n  stringify: __YAML__.stringify,\n  parse: __YAML__.parse,\n  parseAll: __YAML__.parseAll,\n  parseDockerCompose: __YAML__.parse,\n}}\n\nconst TOML = {{\n  stringify: __TOML__.stringify,\n  parse: __TOML__.parse,\n  parseResourceToml: __TOML__.parse,\n  parseCargoToml: __TOML__.parse,\n}}\n\nconst ARGS = {args};\n\nconst komodo = KomodoClient('{base_url}', {{\n  type: 'api-key',\n  params: {{ key: '{key}', secret: '{secret}' }}\n}});\n\nasync function main() {{\n{contents}\n\nconsole.log('🦎 Action completed successfully 🦎');\n}}\n\nmain()\n.catch(error => {{\n  console.error('🚨 Action exited early with errors 🚨')\n  if (error.status !== undefined && error.result !== undefined) {{\n    console.error('Status:', error.status);\n    console.error(JSON.stringify(error.result, null, 2));\n  }} else {{\n    console.error(error);\n  }}\n  Deno.exit(1)\n}});\"\n  )\n}\n\n/// Cleans up file at given path.\n/// ALSO if $DENO_DIR is set,\n/// will clean up the generated file matching \"file\"\nasync fn cleanup_run(file: String, path: &Path) {\n  if let Err(e) = fs::remove_file(path).await {\n    warn!(\n      \"Failed to delete action file after action execution | {e:#}\"\n    );\n  }\n  // If $DENO_DIR is set (will be in container),\n  // will clean up the generated file matching \"file\" (NOT under path)\n  let Some(deno_dir) = deno_dir() else {\n    return;\n  };\n  delete_file(deno_dir.join(\"gen/file\"), file).await;\n}\n\nfn deno_dir() -> Option<&'static Path> {\n  static DENO_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();\n  DENO_DIR\n    .get_or_init(|| {\n      let deno_dir = std::env::var(\"DENO_DIR\").ok()?;\n      PathBuf::from_str(&deno_dir).ok()\n    })\n    .as_deref()\n}\n\n/// file is just the terminating file path,\n/// it may be nested multiple folder under path,\n/// this will find the nested file and delete it.\n/// Assumes the file is only there once.\nfn delete_file(\n  dir: PathBuf,\n  file: String,\n) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send>>\n{\n  Box::pin(async move {\n    let Ok(mut dir) = fs::read_dir(dir).await else {\n      return false;\n    };\n    // Collect the nested folders for recursing\n    // only after checking all the files in directory.\n    let mut folders = Vec::<PathBuf>::new();\n\n    while let Ok(Some(entry)) = dir.next_entry().await {\n      let Ok(meta) = entry.metadata().await else {\n        continue;\n      };\n      if meta.is_file() {\n        let Ok(name) = entry.file_name().into_string() else {\n          continue;\n        };\n        if name == file {\n          if let Err(e) = fs::remove_file(entry.path()).await {\n            warn!(\n              \"Failed to clean up generated file after action execution | {e:#}\"\n            );\n          };\n          return true;\n        }\n      } else {\n        folders.push(entry.path());\n      }\n    }\n\n    if folders.len() == 1 {\n      // unwrap ok, folders definitely is not empty\n      let folder = folders.pop().unwrap();\n      delete_file(folder, file).await\n    } else {\n      // Check folders with file.clone\n      for folder in folders {\n        if delete_file(folder, file.clone()).await {\n          return true;\n        }\n      }\n      false\n    }\n  })\n}\n\nfn parse_action_arguments(\n  args: &str,\n  format: FileFormat,\n) -> anyhow::Result<JsonObject> {\n  match format {\n    FileFormat::KeyValue => {\n      let args = parse_key_value_list(args)\n        .context(\"Failed to parse args as key value list\")?\n        .into_iter()\n        .map(|(k, v)| (k, serde_json::Value::String(v)))\n        .collect();\n      Ok(args)\n    }\n    FileFormat::Toml => toml::from_str(args)\n      .context(\"Failed to parse Toml to Action args\"),\n    FileFormat::Yaml => serde_yaml_ng::from_str(args)\n      .context(\"Failed to parse Yaml to action args\"),\n    FileFormat::Json => serde_json::from_str(args)\n      .context(\"Failed to parse Json to action args\"),\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/alerter.rs",
    "content": "use anyhow::{Context, anyhow};\nuse formatting::format_serror;\nuse futures::{TryStreamExt, stream::FuturesUnordered};\nuse komodo_client::{\n  api::execute::{SendAlert, TestAlerter},\n  entities::{\n    alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},\n    alerter::Alerter,\n    komodo_timestamp,\n    permission::PermissionLevel,\n  },\n};\nuse reqwest::StatusCode;\nuse resolver_api::Resolve;\nuse serror::AddStatusCodeError;\n\nuse crate::{\n  alert::send_alert_to_alerter, helpers::update::update_update,\n  permission::get_check_permissions, resource::list_full_for_user,\n};\n\nuse super::ExecuteArgs;\n\nimpl Resolve<ExecuteArgs> for TestAlerter {\n  #[instrument(name = \"TestAlerter\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> Result<Self::Response, Self::Error> {\n    let alerter = get_check_permissions::<Alerter>(\n      &self.alerter,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    let mut update = update.clone();\n\n    if !alerter.config.enabled {\n      update.push_error_log(\n        \"Test Alerter\",\n        String::from(\n          \"Alerter is disabled. Enable the Alerter to send alerts.\",\n        ),\n      );\n      update.finalize();\n      update_update(update.clone()).await?;\n      return Ok(update);\n    }\n\n    let ts = komodo_timestamp();\n\n    let alert = Alert {\n      id: Default::default(),\n      ts,\n      resolved: true,\n      level: SeverityLevel::Ok,\n      target: update.target.clone(),\n      data: AlertData::Test {\n        id: alerter.id.clone(),\n        name: alerter.name.clone(),\n      },\n      resolved_ts: Some(ts),\n    };\n\n    if let Err(e) = send_alert_to_alerter(&alerter, &alert).await {\n      update.push_error_log(\"Test Alerter\", format_serror(&e.into()));\n    } else {\n      update.push_simple_log(\"Test Alerter\", String::from(\"Alert sent successfully. It should be visible at your alerting destination.\"));\n    };\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\n//\n\nimpl Resolve<ExecuteArgs> for SendAlert {\n  #[instrument(name = \"SendAlert\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> Result<Self::Response, Self::Error> {\n    let alerters = list_full_for_user::<Alerter>(\n      Default::default(),\n      user,\n      PermissionLevel::Execute.into(),\n      &[],\n    )\n    .await?\n    .into_iter()\n    .filter(|a| {\n      a.config.enabled\n        && (self.alerters.is_empty()\n          || self.alerters.contains(&a.name)\n          || self.alerters.contains(&a.id))\n        && (a.config.alert_types.is_empty()\n          || a.config.alert_types.contains(&AlertDataVariant::Custom))\n    })\n    .collect::<Vec<_>>();\n\n    if alerters.is_empty() {\n      return Err(anyhow!(\n        \"Could not find any valid alerters to send to, this required Execute permissions on the Alerter\"\n      ).status_code(StatusCode::BAD_REQUEST));\n    }\n\n    let mut update = update.clone();\n\n    let ts = komodo_timestamp();\n\n    let alert = Alert {\n      id: Default::default(),\n      ts,\n      resolved: true,\n      level: self.level,\n      target: update.target.clone(),\n      data: AlertData::Custom {\n        message: self.message,\n        details: self.details,\n      },\n      resolved_ts: Some(ts),\n    };\n\n    update.push_simple_log(\n      \"Send alert\",\n      serde_json::to_string_pretty(&alert)\n        .context(\"Failed to serialize alert to JSON\")?,\n    );\n\n    if let Err(e) = alerters\n      .iter()\n      .map(|alerter| send_alert_to_alerter(alerter, &alert))\n      .collect::<FuturesUnordered<_>>()\n      .try_collect::<Vec<_>>()\n      .await\n    {\n      update.push_error_log(\"Send Error\", format_serror(&e.into()));\n    };\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/build.rs",
    "content": "use std::{\n  collections::{HashMap, HashSet},\n  future::IntoFuture,\n  time::Duration,\n};\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::update_one_by_id,\n  find::find_collect,\n  mongodb::{\n    bson::{doc, to_bson, to_document},\n    options::FindOneOptions,\n  },\n};\nuse formatting::format_serror;\nuse futures::future::join_all;\nuse interpolate::Interpolator;\nuse komodo_client::{\n  api::execute::{\n    BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,\n    RunBuild,\n  },\n  entities::{\n    alert::{Alert, AlertData, SeverityLevel},\n    all_logs_success,\n    build::{Build, BuildConfig},\n    builder::{Builder, BuilderConfig},\n    deployment::DeploymentState,\n    komodo_timestamp, optional_string,\n    permission::PermissionLevel,\n    repo::Repo,\n    update::{Log, Update},\n    user::auto_redeploy_user,\n  },\n};\nuse periphery_client::api;\nuse resolver_api::Resolve;\nuse tokio_util::sync::CancellationToken;\n\nuse crate::{\n  alert::send_alerts,\n  helpers::{\n    build_git_token,\n    builder::{cleanup_builder_instance, get_builder_periphery},\n    channel::build_cancel_channel,\n    query::{\n      VariablesAndSecrets, get_deployment_state,\n      get_variables_and_secrets,\n    },\n    registry_token,\n    update::{init_execution_update, update_update},\n  },\n  permission::get_check_permissions,\n  resource::{self, refresh_build_state_cache},\n  state::{action_states, db_client},\n};\n\nuse super::{ExecuteArgs, ExecuteRequest};\n\nimpl super::BatchExecute for BatchRunBuild {\n  type Resource = Build;\n  fn single_request(build: String) -> ExecuteRequest {\n    ExecuteRequest::RunBuild(RunBuild { build })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchRunBuild {\n  #[instrument(name = \"BatchRunBuild\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchRunBuild>(&self.pattern, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<ExecuteArgs> for RunBuild {\n  #[instrument(name = \"RunBuild\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let mut build = get_check_permissions::<Build>(\n      &self.build,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    let mut repo = if !build.config.files_on_host\n      && !build.config.linked_repo.is_empty()\n    {\n      crate::resource::get::<Repo>(&build.config.linked_repo)\n        .await?\n        .into()\n    } else {\n      None\n    };\n\n    let VariablesAndSecrets {\n      mut variables,\n      secrets,\n    } = get_variables_and_secrets().await?;\n\n    // Add the $VERSION to variables. Use with [[$VERSION]]\n    variables.insert(\n      String::from(\"$VERSION\"),\n      build.config.version.to_string(),\n    );\n\n    if build.config.builder_id.is_empty() {\n      return Err(anyhow!(\"Must attach builder to RunBuild\").into());\n    }\n\n    // get the action state for the build (or insert default).\n    let action_state =\n      action_states().build.get_or_insert_default(&build.id).await;\n\n    // This will set action state back to default when dropped.\n    // Will also check to ensure build not already busy before updating.\n    let _action_guard =\n      action_state.update(|state| state.building = true)?;\n\n    if build.config.auto_increment_version {\n      build.config.version.increment();\n    }\n\n    let mut update = update.clone();\n\n    update.version = build.config.version;\n    update_update(update.clone()).await?;\n\n    let git_token =\n      build_git_token(&mut build, repo.as_mut()).await?;\n\n    let registry_tokens =\n      validate_account_extract_registry_tokens(&build).await?;\n\n    let cancel = CancellationToken::new();\n    let cancel_clone = cancel.clone();\n    let mut cancel_recv =\n      build_cancel_channel().receiver.resubscribe();\n    let build_id = build.id.clone();\n\n    let builder =\n      resource::get::<Builder>(&build.config.builder_id).await?;\n\n    let is_server_builder =\n      matches!(&builder.config, BuilderConfig::Server(_));\n\n    tokio::spawn(async move {\n      let poll = async {\n        loop {\n          let (incoming_build_id, mut update) = tokio::select! {\n            _ = cancel_clone.cancelled() => return Ok(()),\n            id = cancel_recv.recv() => id?\n          };\n          if incoming_build_id == build_id {\n            if is_server_builder {\n              update.push_error_log(\"Cancel acknowledged\", \"Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature.\");\n            } else {\n              update.push_simple_log(\"Cancel acknowledged\", \"The build cancellation has been queued, it may still take some time.\");\n            }\n            update.finalize();\n            let id = update.id.clone();\n            if let Err(e) = update_update(update).await {\n              warn!(\"failed to modify Update {id} on db | {e:#}\");\n            }\n            if !is_server_builder {\n              cancel_clone.cancel();\n            }\n            return Ok(());\n          }\n        }\n        #[allow(unreachable_code)]\n        anyhow::Ok(())\n      };\n      tokio::select! {\n        _ = cancel_clone.cancelled() => {}\n        _ = poll => {}\n      }\n    });\n\n    // GET BUILDER PERIPHERY\n    let (periphery, cleanup_data) = match get_builder_periphery(\n      build.name.clone(),\n      Some(build.config.version),\n      builder,\n      &mut update,\n    )\n    .await\n    {\n      Ok(builder) => builder,\n      Err(e) => {\n        warn!(\n          \"failed to get builder for build {} | {e:#}\",\n          build.name\n        );\n        update.logs.push(Log::error(\n          \"get builder\",\n          format_serror(&e.context(\"failed to get builder\").into()),\n        ));\n        return handle_early_return(\n          update, build.id, build.name, false,\n        )\n        .await;\n      }\n    };\n\n    // INTERPOLATE VARIABLES\n    let secret_replacers = if !build.config.skip_secret_interp {\n      let mut interpolator =\n        Interpolator::new(Some(&variables), &secrets);\n\n      interpolator.interpolate_build(&mut build)?;\n\n      if let Some(repo) = repo.as_mut() {\n        interpolator.interpolate_repo(repo)?;\n      }\n\n      interpolator.push_logs(&mut update.logs);\n\n      interpolator.secret_replacers\n    } else {\n      Default::default()\n    };\n\n    let commit_message = if !build.config.files_on_host\n      && (!build.config.repo.is_empty()\n        || !build.config.linked_repo.is_empty())\n    {\n      // PULL OR CLONE REPO\n      let res = tokio::select! {\n        res = periphery\n          .request(api::git::PullOrCloneRepo {\n            args: repo.as_ref().map(Into::into).unwrap_or((&build).into()),\n            git_token,\n            environment: Default::default(),\n            env_file_path: Default::default(),\n            on_clone: None,\n            on_pull: None,\n            skip_secret_interp: Default::default(),\n            replacers: Default::default(),\n          }) => res,\n        _ = cancel.cancelled() => {\n          debug!(\"build cancelled during clone, cleaning up builder\");\n          update.push_error_log(\"build cancelled\", String::from(\"user cancelled build during repo clone\"));\n          cleanup_builder_instance(cleanup_data, &mut update)\n            .await;\n          info!(\"builder cleaned up\");\n          return handle_early_return(update, build.id, build.name, true).await\n        },\n      };\n\n      let commit_message = match res {\n        Ok(res) => {\n          debug!(\"finished repo clone\");\n          update.logs.extend(res.res.logs);\n          update.commit_hash =\n            res.res.commit_hash.unwrap_or_default().to_string();\n          res.res.commit_message.unwrap_or_default()\n        }\n        Err(e) => {\n          warn!(\"Failed build at clone repo | {e:#}\");\n          update.push_error_log(\n            \"Clone Repo\",\n            format_serror(&e.context(\"Failed to clone repo\").into()),\n          );\n          Default::default()\n        }\n      };\n\n      update_update(update.clone()).await?;\n\n      Some(commit_message)\n    } else {\n      None\n    };\n\n    if all_logs_success(&update.logs) {\n      // RUN BUILD\n      let res = tokio::select! {\n        res = periphery\n          .request(api::build::Build {\n            build: build.clone(),\n            repo,\n            registry_tokens,\n            replacers: secret_replacers.into_iter().collect(),\n            // To push a commit hash tagged image\n            commit_hash: optional_string(&update.commit_hash),\n            // Unused for now\n            additional_tags: Default::default(),\n          }) => res.context(\"failed at call to periphery to build\"),\n        _ = cancel.cancelled() => {\n          info!(\"build cancelled during build, cleaning up builder\");\n          update.push_error_log(\"build cancelled\", String::from(\"user cancelled build during docker build\"));\n          cleanup_builder_instance(cleanup_data, &mut update)\n            .await;\n          return handle_early_return(update, build.id, build.name, true).await\n        },\n      };\n\n      match res {\n        Ok(logs) => {\n          debug!(\"finished build\");\n          update.logs.extend(logs);\n        }\n        Err(e) => {\n          warn!(\"error in build | {e:#}\");\n          update.push_error_log(\n            \"build\",\n            format_serror(&e.context(\"failed to build\").into()),\n          )\n        }\n      };\n    }\n\n    update.finalize();\n\n    let db = db_client();\n\n    if update.success {\n      let _ = db\n        .builds\n        .update_one(\n          doc! { \"name\": &build.name },\n          doc! { \"$set\": {\n            \"config.version\": to_bson(&build.config.version)\n              .context(\"failed at converting version to bson\")?,\n            \"info.last_built_at\": komodo_timestamp(),\n            \"info.built_hash\": &update.commit_hash,\n            \"info.built_message\": commit_message\n          }},\n        )\n        .await;\n    }\n\n    // stop the cancel listening task from going forever\n    cancel.cancel();\n\n    // If building on temporary cloud server (AWS),\n    // this will terminate the server.\n    cleanup_builder_instance(cleanup_data, &mut update).await;\n\n    // Need to manually update the update before cache refresh,\n    // and before broadcast with add_update.\n    // The Err case of to_document should be unreachable,\n    // but will fail to update cache in that case.\n    if let Ok(update_doc) = to_document(&update) {\n      let _ = update_one_by_id(\n        &db.updates,\n        &update.id,\n        database::mungos::update::Update::Set(update_doc),\n        None,\n      )\n      .await;\n      refresh_build_state_cache().await;\n    }\n\n    update_update(update.clone()).await?;\n\n    if update.success {\n      // don't hold response up for user\n      tokio::spawn(async move {\n        handle_post_build_redeploy(&build.id).await;\n      });\n    } else {\n      warn!(\"build unsuccessful, alerting...\");\n      let target = update.target.clone();\n      let version = update.version;\n      tokio::spawn(async move {\n        let alert = Alert {\n          id: Default::default(),\n          target,\n          ts: komodo_timestamp(),\n          resolved_ts: Some(komodo_timestamp()),\n          resolved: true,\n          level: SeverityLevel::Warning,\n          data: AlertData::BuildFailed {\n            id: build.id,\n            name: build.name,\n            version,\n          },\n        };\n        send_alerts(&[alert]).await\n      });\n    }\n\n    Ok(update.clone())\n  }\n}\n\n#[instrument(skip(update))]\nasync fn handle_early_return(\n  mut update: Update,\n  build_id: String,\n  build_name: String,\n  is_cancel: bool,\n) -> serror::Result<Update> {\n  update.finalize();\n  // Need to manually update the update before cache refresh,\n  // and before broadcast with add_update.\n  // The Err case of to_document should be unreachable,\n  // but will fail to update cache in that case.\n  if let Ok(update_doc) = to_document(&update) {\n    let _ = update_one_by_id(\n      &db_client().updates,\n      &update.id,\n      database::mungos::update::Update::Set(update_doc),\n      None,\n    )\n    .await;\n    refresh_build_state_cache().await;\n  }\n  update_update(update.clone()).await?;\n  if !update.success && !is_cancel {\n    warn!(\"build unsuccessful, alerting...\");\n    let target = update.target.clone();\n    let version = update.version;\n    tokio::spawn(async move {\n      let alert = Alert {\n        id: Default::default(),\n        target,\n        ts: komodo_timestamp(),\n        resolved_ts: Some(komodo_timestamp()),\n        resolved: true,\n        level: SeverityLevel::Warning,\n        data: AlertData::BuildFailed {\n          id: build_id,\n          name: build_name,\n          version,\n        },\n      };\n      send_alerts(&[alert]).await\n    });\n  }\n  Ok(update.clone())\n}\n\npub async fn validate_cancel_build(\n  request: &ExecuteRequest,\n) -> anyhow::Result<()> {\n  if let ExecuteRequest::CancelBuild(req) = request {\n    let build = resource::get::<Build>(&req.build).await?;\n\n    let db = db_client();\n\n    let (latest_build, latest_cancel) = tokio::try_join!(\n      db.updates\n        .find_one(doc! {\n          \"operation\": \"RunBuild\",\n          \"target.id\": &build.id,\n        },)\n        .with_options(\n          FindOneOptions::builder()\n            .sort(doc! { \"start_ts\": -1 })\n            .build()\n        )\n        .into_future(),\n      db.updates\n        .find_one(doc! {\n          \"operation\": \"CancelBuild\",\n          \"target.id\": &build.id,\n        },)\n        .with_options(\n          FindOneOptions::builder()\n            .sort(doc! { \"start_ts\": -1 })\n            .build()\n        )\n        .into_future()\n    )?;\n\n    match (latest_build, latest_cancel) {\n      (Some(build), Some(cancel)) => {\n        if cancel.start_ts > build.start_ts {\n          return Err(anyhow!(\"Build has already been cancelled\"));\n        }\n      }\n      (None, _) => return Err(anyhow!(\"No build in progress\")),\n      _ => {}\n    };\n  }\n  Ok(())\n}\n\nimpl Resolve<ExecuteArgs> for CancelBuild {\n  #[instrument(name = \"CancelBuild\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let build = get_check_permissions::<Build>(\n      &self.build,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // make sure the build is building\n    if !action_states()\n      .build\n      .get(&build.id)\n      .await\n      .and_then(|s| s.get().ok().map(|s| s.building))\n      .unwrap_or_default()\n    {\n      return Err(anyhow!(\"Build is not building.\").into());\n    }\n\n    let mut update = update.clone();\n\n    update.push_simple_log(\n      \"cancel triggered\",\n      \"the build cancel has been triggered\",\n    );\n    update_update(update.clone()).await?;\n\n    build_cancel_channel()\n      .sender\n      .lock()\n      .await\n      .send((build.id, update.clone()))?;\n\n    // Make sure cancel is set to complete after some time in case\n    // no reciever is there to do it. Prevents update stuck in InProgress.\n    let update_id = update.id.clone();\n    tokio::spawn(async move {\n      tokio::time::sleep(Duration::from_secs(60)).await;\n      if let Err(e) = update_one_by_id(\n        &db_client().updates,\n        &update_id,\n        doc! { \"$set\": { \"status\": \"Complete\" } },\n        None,\n      )\n      .await\n      {\n        warn!(\n          \"failed to set CancelBuild Update status Complete after timeout | {e:#}\"\n        )\n      }\n    });\n\n    Ok(update)\n  }\n}\n\n#[instrument]\nasync fn handle_post_build_redeploy(build_id: &str) {\n  let Ok(redeploy_deployments) = find_collect(\n    &db_client().deployments,\n    doc! {\n      \"config.image.params.build_id\": build_id,\n      \"config.redeploy_on_build\": true\n    },\n    None,\n  )\n  .await\n  else {\n    return;\n  };\n\n  let futures =\n    redeploy_deployments\n      .into_iter()\n      .map(|deployment| async move {\n        let state = get_deployment_state(&deployment.id)\n          .await\n          .unwrap_or_default();\n        if state == DeploymentState::Running {\n          let req = super::ExecuteRequest::Deploy(Deploy {\n            deployment: deployment.id.clone(),\n            stop_signal: None,\n            stop_time: None,\n          });\n          let user = auto_redeploy_user().to_owned();\n          let res = async {\n            let update = init_execution_update(&req, &user).await?;\n            Deploy {\n              deployment: deployment.id.clone(),\n              stop_signal: None,\n              stop_time: None,\n            }\n            .resolve(&ExecuteArgs { user, update })\n            .await\n          }\n          .await;\n          Some((deployment.id.clone(), res))\n        } else {\n          None\n        }\n      });\n\n  for res in join_all(futures).await {\n    let Some((id, res)) = res else {\n      continue;\n    };\n    if let Err(e) = res {\n      warn!(\n        \"failed post build redeploy for deployment {id}: {:#}\",\n        e.error\n      );\n    }\n  }\n}\n\n/// This will make sure that a build with non-none image registry has an account attached,\n/// and will check the core config for a token matching requirements.\n/// Otherwise it is left to periphery.\nasync fn validate_account_extract_registry_tokens(\n  Build {\n    config: BuildConfig { image_registry, .. },\n    ..\n  }: &Build,\n  // Maps (domain, account) -> token\n) -> serror::Result<Vec<(String, String, String)>> {\n  let mut res = HashMap::with_capacity(image_registry.capacity());\n\n  for (domain, account) in image_registry\n    .iter()\n    .map(|r| (r.domain.as_str(), r.account.as_str()))\n    // This ensures uniqueness / prevents redundant logins\n    .collect::<HashSet<_>>()\n  {\n    if domain.is_empty() {\n      continue;\n    }\n    if account.is_empty() {\n      return Err(\n        anyhow!(\n          \"Must attach account to use registry provider {domain}\"\n        )\n        .into(),\n      );\n    }\n    let Some(registry_token) = registry_token(domain, account).await.with_context(\n      || format!(\"Failed to get registry token in call to db. Stopping run. | {domain} | {account}\"),\n    )? else {\n      continue;\n    };\n\n    res.insert(\n      (domain.to_string(), account.to_string()),\n      registry_token,\n    );\n  }\n\n  Ok(\n    res\n      .into_iter()\n      .map(|((domain, account), token)| (domain, account, token))\n      .collect(),\n  )\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/deployment.rs",
    "content": "use std::sync::OnceLock;\n\nuse anyhow::{Context, anyhow};\nuse cache::TimeoutCache;\nuse formatting::format_serror;\nuse interpolate::Interpolator;\nuse komodo_client::{\n  api::execute::*,\n  entities::{\n    Version,\n    build::{Build, ImageRegistryConfig},\n    deployment::{\n      Deployment, DeploymentImage, extract_registry_domain,\n    },\n    komodo_timestamp, optional_string,\n    permission::PermissionLevel,\n    server::Server,\n    update::{Log, Update},\n    user::User,\n  },\n};\nuse periphery_client::api;\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::{\n    periphery_client,\n    query::{VariablesAndSecrets, get_variables_and_secrets},\n    registry_token,\n    update::update_update,\n  },\n  monitor::update_cache_for_server,\n  permission::get_check_permissions,\n  resource,\n  state::action_states,\n};\n\nuse super::{ExecuteArgs, ExecuteRequest};\n\nimpl super::BatchExecute for BatchDeploy {\n  type Resource = Deployment;\n  fn single_request(deployment: String) -> ExecuteRequest {\n    ExecuteRequest::Deploy(Deploy {\n      deployment,\n      stop_signal: None,\n      stop_time: None,\n    })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchDeploy {\n  #[instrument(name = \"BatchDeploy\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchDeploy>(&self.pattern, user)\n        .await?,\n    )\n  }\n}\n\nasync fn setup_deployment_execution(\n  deployment: &str,\n  user: &User,\n) -> anyhow::Result<(Deployment, Server)> {\n  let deployment = get_check_permissions::<Deployment>(\n    deployment,\n    user,\n    PermissionLevel::Execute.into(),\n  )\n  .await?;\n\n  if deployment.config.server_id.is_empty() {\n    return Err(anyhow!(\"Deployment has no Server configured\"));\n  }\n\n  let server =\n    resource::get::<Server>(&deployment.config.server_id).await?;\n\n  if !server.config.enabled {\n    return Err(anyhow!(\"Attached Server is not enabled\"));\n  }\n\n  Ok((deployment, server))\n}\n\nimpl Resolve<ExecuteArgs> for Deploy {\n  #[instrument(name = \"Deploy\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (mut deployment, server) =\n      setup_deployment_execution(&self.deployment, user).await?;\n\n    // get the action state for the deployment (or insert default).\n    let action_state = action_states()\n      .deployment\n      .get_or_insert_default(&deployment.id)\n      .await;\n\n    // Will check to ensure deployment not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.deploying = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    // This block resolves the attached Build to an actual versioned image\n    let (version, registry_token) = match &deployment.config.image {\n      DeploymentImage::Build { build_id, version } => {\n        let build = resource::get::<Build>(build_id).await?;\n        let image_names = build.get_image_names();\n        let image_name = image_names\n          .first()\n          .context(\"No image name could be created\")\n          .context(\"Failed to create image name\")?;\n        let version = if version.is_none() {\n          build.config.version\n        } else {\n          *version\n        };\n        let version_str = version.to_string();\n        // Potentially add the build image_tag postfix\n        let version_str = if build.config.image_tag.is_empty() {\n          version_str\n        } else {\n          format!(\"{version_str}-{}\", build.config.image_tag)\n        };\n        // replace image with corresponding build image.\n        deployment.config.image = DeploymentImage::Image {\n          image: format!(\"{image_name}:{version_str}\"),\n        };\n        let first_registry = build\n          .config\n          .image_registry\n          .first()\n          .unwrap_or(ImageRegistryConfig::static_default());\n        if first_registry.domain.is_empty() {\n          (version, None)\n        } else {\n          let ImageRegistryConfig {\n            domain, account, ..\n          } = first_registry;\n          if deployment.config.image_registry_account.is_empty() {\n            deployment.config.image_registry_account =\n              account.to_string();\n          }\n          let token = if !deployment\n            .config\n            .image_registry_account\n            .is_empty()\n          {\n            registry_token(domain, &deployment.config.image_registry_account).await.with_context(\n              || format!(\"Failed to get git token in call to db. Stopping run. | {domain} | {}\", deployment.config.image_registry_account),\n            )?\n          } else {\n            None\n          };\n          (version, token)\n        }\n      }\n      DeploymentImage::Image { image } => {\n        let domain = extract_registry_domain(image)?;\n        let token = if !deployment\n          .config\n          .image_registry_account\n          .is_empty()\n        {\n          registry_token(&domain, &deployment.config.image_registry_account).await.with_context(\n            || format!(\"Failed to get git token in call to db. Stopping run. | {domain} | {}\", deployment.config.image_registry_account),\n          )?\n        } else {\n          None\n        };\n        (Version::default(), token)\n      }\n    };\n\n    // interpolate variables / secrets, returning the sanitizing replacers to send to\n    // periphery so it may sanitize the final command for safe logging (avoids exposing secret values)\n    let secret_replacers = if !deployment.config.skip_secret_interp {\n      let VariablesAndSecrets { variables, secrets } =\n        get_variables_and_secrets().await?;\n\n      let mut interpolator =\n        Interpolator::new(Some(&variables), &secrets);\n\n      interpolator\n        .interpolate_deployment(&mut deployment)?\n        .push_logs(&mut update.logs);\n\n      interpolator.secret_replacers\n    } else {\n      Default::default()\n    };\n\n    update.version = version;\n    update_update(update.clone()).await?;\n\n    match periphery_client(&server)?\n      .request(api::container::Deploy {\n        deployment,\n        stop_signal: self.stop_signal,\n        stop_time: self.stop_time,\n        registry_token,\n        replacers: secret_replacers.into_iter().collect(),\n      })\n      .await\n    {\n      Ok(log) => update.logs.push(log),\n      Err(e) => {\n        update.push_error_log(\n          \"Deploy Container\",\n          format_serror(&e.into()),\n        );\n      }\n    };\n\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\n/// Wait this long after a pull to allow another pull through\nconst PULL_TIMEOUT: i64 = 5_000;\ntype ServerId = String;\ntype Image = String;\ntype PullCache = TimeoutCache<(ServerId, Image), Log>;\n\nfn pull_cache() -> &'static PullCache {\n  static PULL_CACHE: OnceLock<PullCache> = OnceLock::new();\n  PULL_CACHE.get_or_init(Default::default)\n}\n\npub async fn pull_deployment_inner(\n  deployment: Deployment,\n  server: &Server,\n) -> anyhow::Result<Log> {\n  let (image, account, token) = match deployment.config.image {\n    DeploymentImage::Build { build_id, version } => {\n      let build = resource::get::<Build>(&build_id).await?;\n      let image_names = build.get_image_names();\n      let image_name = image_names\n        .first()\n        .context(\"No image name could be created\")\n        .context(\"Failed to create image name\")?;\n      let version = if version.is_none() {\n        build.config.version.to_string()\n      } else {\n        version.to_string()\n      };\n      // Potentially add the build image_tag postfix\n      let version = if build.config.image_tag.is_empty() {\n        version\n      } else {\n        format!(\"{version}-{}\", build.config.image_tag)\n      };\n      // replace image with corresponding build image.\n      let image = format!(\"{image_name}:{version}\");\n      let first_registry = build\n        .config\n        .image_registry\n        .first()\n        .unwrap_or(ImageRegistryConfig::static_default());\n      if first_registry.domain.is_empty() {\n        (image, None, None)\n      } else {\n        let ImageRegistryConfig {\n          domain, account, ..\n        } = first_registry;\n        let account =\n          if deployment.config.image_registry_account.is_empty() {\n            account\n          } else {\n            &deployment.config.image_registry_account\n          };\n        let token = if !account.is_empty() {\n          registry_token(domain, account).await.with_context(\n              || format!(\"Failed to get git token in call to db. Stopping run. | {domain} | {account}\"),\n            )?\n        } else {\n          None\n        };\n        (image, optional_string(account), token)\n      }\n    }\n    DeploymentImage::Image { image } => {\n      let domain = extract_registry_domain(&image)?;\n      let token = if !deployment\n        .config\n        .image_registry_account\n        .is_empty()\n      {\n        registry_token(&domain, &deployment.config.image_registry_account).await.with_context(\n            || format!(\"Failed to get git token in call to db. Stopping run. | {domain} | {}\", deployment.config.image_registry_account),\n          )?\n      } else {\n        None\n      };\n      (\n        image,\n        optional_string(&deployment.config.image_registry_account),\n        token,\n      )\n    }\n  };\n\n  // Acquire the pull lock for this image on the server\n  let lock = pull_cache()\n    .get_lock((server.id.clone(), image.clone()))\n    .await;\n\n  // Lock the path lock, prevents simultaneous pulls by\n  // ensuring simultaneous pulls will wait for first to finish\n  // and checking cached results.\n  let mut locked = lock.lock().await;\n\n  // Early return from cache if lasted pulled with PULL_TIMEOUT\n  if locked.last_ts + PULL_TIMEOUT > komodo_timestamp() {\n    return locked.clone_res();\n  }\n\n  let res = async {\n    let log = match periphery_client(server)?\n      .request(api::image::PullImage {\n        name: image,\n        account,\n        token,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\"Pull image\", format_serror(&e.into())),\n    };\n\n    update_cache_for_server(server, true).await;\n    anyhow::Ok(log)\n  }\n  .await;\n\n  // Set the cache with results. Any other calls waiting on the lock will\n  // then immediately also use this same result.\n  locked.set(&res, komodo_timestamp());\n\n  res\n}\n\nimpl Resolve<ExecuteArgs> for PullDeployment {\n  #[instrument(name = \"PullDeployment\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (deployment, server) =\n      setup_deployment_execution(&self.deployment, user).await?;\n\n    // get the action state for the deployment (or insert default).\n    let action_state = action_states()\n      .deployment\n      .get_or_insert_default(&deployment.id)\n      .await;\n\n    // Will check to ensure deployment not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pulling = true)?;\n\n    let mut update = update.clone();\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let log = pull_deployment_inner(deployment, &server).await?;\n\n    update.logs.push(log);\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for StartDeployment {\n  #[instrument(name = \"StartDeployment\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (deployment, server) =\n      setup_deployment_execution(&self.deployment, user).await?;\n\n    // get the action state for the deployment (or insert default).\n    let action_state = action_states()\n      .deployment\n      .get_or_insert_default(&deployment.id)\n      .await;\n\n    // Will check to ensure deployment not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.starting = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let log = match periphery_client(&server)?\n      .request(api::container::StartContainer {\n        name: deployment.name,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"start container\",\n        format_serror(&e.context(\"failed to start container\").into()),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for RestartDeployment {\n  #[instrument(name = \"RestartDeployment\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (deployment, server) =\n      setup_deployment_execution(&self.deployment, user).await?;\n\n    // get the action state for the deployment (or insert default).\n    let action_state = action_states()\n      .deployment\n      .get_or_insert_default(&deployment.id)\n      .await;\n\n    // Will check to ensure deployment not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.restarting = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let log = match periphery_client(&server)?\n      .request(api::container::RestartContainer {\n        name: deployment.name,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"restart container\",\n        format_serror(\n          &e.context(\"failed to restart container\").into(),\n        ),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PauseDeployment {\n  #[instrument(name = \"PauseDeployment\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (deployment, server) =\n      setup_deployment_execution(&self.deployment, user).await?;\n\n    // get the action state for the deployment (or insert default).\n    let action_state = action_states()\n      .deployment\n      .get_or_insert_default(&deployment.id)\n      .await;\n\n    // Will check to ensure deployment not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pausing = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let log = match periphery_client(&server)?\n      .request(api::container::PauseContainer {\n        name: deployment.name,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"pause container\",\n        format_serror(&e.context(\"failed to pause container\").into()),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for UnpauseDeployment {\n  #[instrument(name = \"UnpauseDeployment\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (deployment, server) =\n      setup_deployment_execution(&self.deployment, user).await?;\n\n    // get the action state for the deployment (or insert default).\n    let action_state = action_states()\n      .deployment\n      .get_or_insert_default(&deployment.id)\n      .await;\n\n    // Will check to ensure deployment not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.unpausing = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let log = match periphery_client(&server)?\n      .request(api::container::UnpauseContainer {\n        name: deployment.name,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"unpause container\",\n        format_serror(\n          &e.context(\"failed to unpause container\").into(),\n        ),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for StopDeployment {\n  #[instrument(name = \"StopDeployment\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (deployment, server) =\n      setup_deployment_execution(&self.deployment, user).await?;\n\n    // get the action state for the deployment (or insert default).\n    let action_state = action_states()\n      .deployment\n      .get_or_insert_default(&deployment.id)\n      .await;\n\n    // Will check to ensure deployment not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.stopping = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let log = match periphery_client(&server)?\n      .request(api::container::StopContainer {\n        name: deployment.name,\n        signal: self\n          .signal\n          .unwrap_or(deployment.config.termination_signal)\n          .into(),\n        time: self\n          .time\n          .unwrap_or(deployment.config.termination_timeout)\n          .into(),\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"stop container\",\n        format_serror(&e.context(\"failed to stop container\").into()),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl super::BatchExecute for BatchDestroyDeployment {\n  type Resource = Deployment;\n  fn single_request(deployment: String) -> ExecuteRequest {\n    ExecuteRequest::DestroyDeployment(DestroyDeployment {\n      deployment,\n      signal: None,\n      time: None,\n    })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchDestroyDeployment {\n  #[instrument(name = \"BatchDestroyDeployment\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchDestroyDeployment>(\n        &self.pattern,\n        user,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ExecuteArgs> for DestroyDeployment {\n  #[instrument(name = \"DestroyDeployment\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (deployment, server) =\n      setup_deployment_execution(&self.deployment, user).await?;\n\n    // get the action state for the deployment (or insert default).\n    let action_state = action_states()\n      .deployment\n      .get_or_insert_default(&deployment.id)\n      .await;\n\n    // Will check to ensure deployment not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.destroying = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let log = match periphery_client(&server)?\n      .request(api::container::RemoveContainer {\n        name: deployment.name,\n        signal: self\n          .signal\n          .unwrap_or(deployment.config.termination_signal)\n          .into(),\n        time: self\n          .time\n          .unwrap_or(deployment.config.termination_timeout)\n          .into(),\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"stop container\",\n        format_serror(&e.context(\"failed to stop container\").into()),\n      ),\n    };\n\n    update.logs.push(log);\n    update.finalize();\n    update_cache_for_server(&server, true).await;\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/maintenance.rs",
    "content": "use std::sync::OnceLock;\n\nuse anyhow::{Context, anyhow};\nuse command::run_komodo_command;\nuse database::mungos::{find::find_collect, mongodb::bson::doc};\nuse formatting::{bold, format_serror};\nuse komodo_client::{\n  api::execute::{\n    BackupCoreDatabase, ClearRepoCache, GlobalAutoUpdate,\n  },\n  entities::{\n    deployment::DeploymentState, server::ServerState,\n    stack::StackState,\n  },\n};\nuse reqwest::StatusCode;\nuse resolver_api::Resolve;\nuse serror::AddStatusCodeError;\nuse tokio::sync::Mutex;\n\nuse crate::{\n  api::execute::{\n    ExecuteArgs, pull_deployment_inner, pull_stack_inner,\n  },\n  config::core_config,\n  helpers::update::update_update,\n  state::{\n    db_client, deployment_status_cache, server_status_cache,\n    stack_status_cache,\n  },\n};\n\n/// Makes sure the method can only be called once at a time\nfn clear_repo_cache_lock() -> &'static Mutex<()> {\n  static LOCK: OnceLock<Mutex<()>> = OnceLock::new();\n  LOCK.get_or_init(Default::default)\n}\n\nimpl Resolve<ExecuteArgs> for ClearRepoCache {\n  #[instrument(\n    name = \"ClearRepoCache\",\n    skip(user, update),\n    fields(user_id = user.id, update_id = update.id)\n  )]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> Result<Self::Response, Self::Error> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"This method is admin only.\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    let _lock = clear_repo_cache_lock()\n      .try_lock()\n      .context(\"Clear already in progress...\")?;\n\n    let mut update = update.clone();\n\n    let mut contents =\n      tokio::fs::read_dir(&core_config().repo_directory)\n        .await\n        .context(\"Failed to read repo cache directory\")?;\n\n    loop {\n      let path = match contents\n        .next_entry()\n        .await\n        .context(\"Failed to read contents at path\")\n      {\n        Ok(Some(contents)) => contents.path(),\n        Ok(None) => break,\n        Err(e) => {\n          update.push_error_log(\n            \"Read Directory\",\n            format_serror(&e.into()),\n          );\n          continue;\n        }\n      };\n      if path.is_dir() {\n        match tokio::fs::remove_dir_all(&path)\n          .await\n          .context(\"Failed to clear contents at path\")\n        {\n          Ok(_) => {}\n          Err(e) => {\n            update.push_error_log(\n              \"Clear Directory\",\n              format_serror(&e.into()),\n            );\n          }\n        };\n      }\n    }\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\n//\n\n/// Makes sure the method can only be called once at a time\nfn backup_database_lock() -> &'static Mutex<()> {\n  static LOCK: OnceLock<Mutex<()>> = OnceLock::new();\n  LOCK.get_or_init(Default::default)\n}\n\nimpl Resolve<ExecuteArgs> for BackupCoreDatabase {\n  #[instrument(\n    name = \"BackupCoreDatabase\",\n    skip(user, update),\n    fields(user_id = user.id, update_id = update.id)\n  )]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> Result<Self::Response, Self::Error> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"This method is admin only.\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    let _lock = backup_database_lock()\n      .try_lock()\n      .context(\"Backup already in progress...\")?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let res = run_komodo_command(\n      \"Backup Core Database\",\n      None,\n      \"km database backup --yes\",\n    )\n    .await;\n\n    update.logs.push(res);\n    update.finalize();\n\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\n//\n\n/// Makes sure the method can only be called once at a time\nfn global_update_lock() -> &'static Mutex<()> {\n  static LOCK: OnceLock<Mutex<()>> = OnceLock::new();\n  LOCK.get_or_init(Default::default)\n}\n\nimpl Resolve<ExecuteArgs> for GlobalAutoUpdate {\n  #[instrument(\n    name = \"GlobalAutoUpdate\",\n    skip(user, update),\n    fields(user_id = user.id, update_id = update.id)\n  )]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> Result<Self::Response, Self::Error> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"This method is admin only.\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    let _lock = global_update_lock()\n      .try_lock()\n      .context(\"Global update already in progress...\")?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    // This is all done in sequence because there is no rush,\n    // the pulls / deploys happen spaced out to ease the load on system.\n    let servers = find_collect(&db_client().servers, None, None)\n      .await\n      .context(\"Failed to query for servers from database\")?;\n\n    let query = doc! {\n      \"$or\": [\n        { \"config.poll_for_updates\": true },\n        { \"config.auto_update\": true }\n      ]\n    };\n\n    let (stacks, repos) = tokio::try_join!(\n      find_collect(&db_client().stacks, query.clone(), None),\n      find_collect(&db_client().repos, None, None)\n    )\n    .context(\"Failed to query for resources from database\")?;\n\n    let server_status_cache = server_status_cache();\n    let stack_status_cache = stack_status_cache();\n\n    // Will be edited later at update.logs[0]\n    update.push_simple_log(\"Auto Pull\", String::new());\n\n    for stack in stacks {\n      let Some(status) = stack_status_cache.get(&stack.id).await\n      else {\n        continue;\n      };\n      // Only pull running stacks.\n      if !matches!(status.curr.state, StackState::Running) {\n        continue;\n      }\n      if let Some(server) =\n        servers.iter().find(|s| s.id == stack.config.server_id)\n        // This check is probably redundant along with running check\n        // but shouldn't hurt\n        && server_status_cache\n          .get(&server.id)\n          .await\n          .map(|s| matches!(s.state, ServerState::Ok))\n          .unwrap_or_default()\n      {\n        let name = stack.name.clone();\n        let repo = if stack.config.linked_repo.is_empty() {\n          None\n        } else {\n          let Some(repo) =\n            repos.iter().find(|r| r.id == stack.config.linked_repo)\n          else {\n            update.push_error_log(\n              &format!(\"Pull Stack {name}\"),\n              format!(\n                \"Did not find any Repo matching {}\",\n                stack.config.linked_repo\n              ),\n            );\n            continue;\n          };\n          Some(repo.clone())\n        };\n        if let Err(e) =\n          pull_stack_inner(stack, Vec::new(), server, repo, None)\n            .await\n        {\n          update.push_error_log(\n            &format!(\"Pull Stack {name}\"),\n            format_serror(&e.into()),\n          );\n        } else {\n          if !update.logs[0].stdout.is_empty() {\n            update.logs[0].stdout.push('\\n');\n          }\n          update.logs[0]\n            .stdout\n            .push_str(&format!(\"Pulled Stack {} ✅\", bold(name)));\n        }\n      }\n    }\n\n    let deployment_status_cache = deployment_status_cache();\n    let deployments =\n      find_collect(&db_client().deployments, query, None)\n        .await\n        .context(\"Failed to query for deployments from database\")?;\n    for deployment in deployments {\n      let Some(status) =\n        deployment_status_cache.get(&deployment.id).await\n      else {\n        continue;\n      };\n      // Only pull running deployments.\n      if !matches!(status.curr.state, DeploymentState::Running) {\n        continue;\n      }\n      if let Some(server) =\n        servers.iter().find(|s| s.id == deployment.config.server_id)\n        // This check is probably redundant along with running check\n        // but shouldn't hurt\n        && server_status_cache\n          .get(&server.id)\n          .await\n          .map(|s| matches!(s.state, ServerState::Ok))\n          .unwrap_or_default()\n      {\n        let name = deployment.name.clone();\n        if let Err(e) =\n          pull_deployment_inner(deployment, server).await\n        {\n          update.push_error_log(\n            &format!(\"Pull Deployment {name}\"),\n            format_serror(&e.into()),\n          );\n        } else {\n          if !update.logs[0].stdout.is_empty() {\n            update.logs[0].stdout.push('\\n');\n          }\n          update.logs[0].stdout.push_str(&format!(\n            \"Pulled Deployment {} ✅\",\n            bold(name)\n          ));\n        }\n      }\n    }\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/mod.rs",
    "content": "use std::{pin::Pin, time::Instant};\n\nuse anyhow::Context;\nuse axum::{\n  Extension, Router, extract::Path, middleware, routing::post,\n};\nuse axum_extra::{TypedHeader, headers::ContentType};\nuse database::mungos::by_id::find_one_by_id;\nuse derive_variants::{EnumVariants, ExtractVariant};\nuse formatting::format_serror;\nuse futures::future::join_all;\nuse komodo_client::{\n  api::execute::*,\n  entities::{\n    Operation,\n    permission::PermissionLevel,\n    update::{Log, Update},\n    user::User,\n  },\n};\nuse resolver_api::Resolve;\nuse response::JsonString;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse serror::Json;\nuse typeshare::typeshare;\nuse uuid::Uuid;\n\nuse crate::{\n  auth::auth_request,\n  helpers::update::{init_execution_update, update_update},\n  resource::{KomodoResource, list_full_for_user_using_pattern},\n  state::db_client,\n};\n\nmod action;\nmod alerter;\nmod build;\nmod deployment;\nmod maintenance;\nmod procedure;\nmod repo;\nmod server;\nmod stack;\nmod sync;\n\nuse super::Variant;\n\npub use {\n  deployment::pull_deployment_inner, stack::pull_stack_inner,\n};\n\npub struct ExecuteArgs {\n  pub user: User,\n  pub update: Update,\n}\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,\n)]\n#[variant_derive(Debug)]\n#[args(ExecuteArgs)]\n#[response(JsonString)]\n#[error(serror::Error)]\n#[serde(tag = \"type\", content = \"params\")]\npub enum ExecuteRequest {\n  // ==== SERVER ====\n  StartContainer(StartContainer),\n  RestartContainer(RestartContainer),\n  PauseContainer(PauseContainer),\n  UnpauseContainer(UnpauseContainer),\n  StopContainer(StopContainer),\n  DestroyContainer(DestroyContainer),\n  StartAllContainers(StartAllContainers),\n  RestartAllContainers(RestartAllContainers),\n  PauseAllContainers(PauseAllContainers),\n  UnpauseAllContainers(UnpauseAllContainers),\n  StopAllContainers(StopAllContainers),\n  PruneContainers(PruneContainers),\n  DeleteNetwork(DeleteNetwork),\n  PruneNetworks(PruneNetworks),\n  DeleteImage(DeleteImage),\n  PruneImages(PruneImages),\n  DeleteVolume(DeleteVolume),\n  PruneVolumes(PruneVolumes),\n  PruneDockerBuilders(PruneDockerBuilders),\n  PruneBuildx(PruneBuildx),\n  PruneSystem(PruneSystem),\n\n  // ==== STACK ====\n  DeployStack(DeployStack),\n  BatchDeployStack(BatchDeployStack),\n  DeployStackIfChanged(DeployStackIfChanged),\n  BatchDeployStackIfChanged(BatchDeployStackIfChanged),\n  PullStack(PullStack),\n  BatchPullStack(BatchPullStack),\n  StartStack(StartStack),\n  RestartStack(RestartStack),\n  StopStack(StopStack),\n  PauseStack(PauseStack),\n  UnpauseStack(UnpauseStack),\n  DestroyStack(DestroyStack),\n  BatchDestroyStack(BatchDestroyStack),\n  RunStackService(RunStackService),\n\n  // ==== DEPLOYMENT ====\n  Deploy(Deploy),\n  BatchDeploy(BatchDeploy),\n  PullDeployment(PullDeployment),\n  StartDeployment(StartDeployment),\n  RestartDeployment(RestartDeployment),\n  PauseDeployment(PauseDeployment),\n  UnpauseDeployment(UnpauseDeployment),\n  StopDeployment(StopDeployment),\n  DestroyDeployment(DestroyDeployment),\n  BatchDestroyDeployment(BatchDestroyDeployment),\n\n  // ==== BUILD ====\n  RunBuild(RunBuild),\n  BatchRunBuild(BatchRunBuild),\n  CancelBuild(CancelBuild),\n\n  // ==== REPO ====\n  CloneRepo(CloneRepo),\n  BatchCloneRepo(BatchCloneRepo),\n  PullRepo(PullRepo),\n  BatchPullRepo(BatchPullRepo),\n  BuildRepo(BuildRepo),\n  BatchBuildRepo(BatchBuildRepo),\n  CancelRepoBuild(CancelRepoBuild),\n\n  // ==== PROCEDURE ====\n  RunProcedure(RunProcedure),\n  BatchRunProcedure(BatchRunProcedure),\n\n  // ==== ACTION ====\n  RunAction(RunAction),\n  BatchRunAction(BatchRunAction),\n\n  // ==== ALERTER ====\n  TestAlerter(TestAlerter),\n  SendAlert(SendAlert),\n\n  // ==== SYNC ====\n  RunSync(RunSync),\n\n  // ==== MAINTENANCE ====\n  ClearRepoCache(ClearRepoCache),\n  BackupCoreDatabase(BackupCoreDatabase),\n  GlobalAutoUpdate(GlobalAutoUpdate),\n}\n\npub fn router() -> Router {\n  Router::new()\n    .route(\"/\", post(handler))\n    .route(\"/{variant}\", post(variant_handler))\n    .layer(middleware::from_fn(auth_request))\n}\n\nasync fn variant_handler(\n  user: Extension<User>,\n  Path(Variant { variant }): Path<Variant>,\n  Json(params): Json<serde_json::Value>,\n) -> serror::Result<(TypedHeader<ContentType>, String)> {\n  let req: ExecuteRequest = serde_json::from_value(json!({\n    \"type\": variant,\n    \"params\": params,\n  }))?;\n  handler(user, Json(req)).await\n}\n\nasync fn handler(\n  Extension(user): Extension<User>,\n  Json(request): Json<ExecuteRequest>,\n) -> serror::Result<(TypedHeader<ContentType>, String)> {\n  let res = match inner_handler(request, user).await? {\n    ExecutionResult::Single(update) => serde_json::to_string(&update)\n      .context(\"Failed to serialize Update\")?,\n    ExecutionResult::Batch(res) => res,\n  };\n  Ok((TypedHeader(ContentType::json()), res))\n}\n\n#[typeshare(serialized_as = \"Update\")]\ntype BoxUpdate = Box<Update>;\n\npub enum ExecutionResult {\n  Single(BoxUpdate),\n  /// The batch contents will be pre serialized here\n  Batch(String),\n}\n\npub fn inner_handler(\n  request: ExecuteRequest,\n  user: User,\n) -> Pin<\n  Box<\n    dyn std::future::Future<Output = anyhow::Result<ExecutionResult>>\n      + Send,\n  >,\n> {\n  Box::pin(async move {\n    let req_id = Uuid::new_v4();\n\n    // Need to validate no cancel is active before any update is created.\n    // This ensures no double update created if Cancel is called more than once for the same request.\n    build::validate_cancel_build(&request).await?;\n    repo::validate_cancel_repo_build(&request).await?;\n\n    let update = init_execution_update(&request, &user).await?;\n\n    // This will be the case for the Batch exections,\n    // they don't have their own updates.\n    // The batch calls also call \"inner_handler\" themselves,\n    // and in their case will spawn tasks, so that isn't necessary\n    // here either.\n    if update.operation == Operation::None {\n      return Ok(ExecutionResult::Batch(\n        task(req_id, request, user, update).await?,\n      ));\n    }\n\n    // Spawn a task for the execution which continues\n    // running after this method returns.\n    let handle =\n      tokio::spawn(task(req_id, request, user, update.clone()));\n\n    // Spawns another task to monitor the first for failures,\n    // and add the log to Update about it (which primary task can't do because it errored out)\n    tokio::spawn({\n      let update_id = update.id.clone();\n      async move {\n        let log = match handle.await {\n          Ok(Err(e)) => {\n            warn!(\"/execute request {req_id} task error: {e:#}\",);\n            Log::error(\"Task Error\", format_serror(&e.into()))\n          }\n          Err(e) => {\n            warn!(\"/execute request {req_id} spawn error: {e:?}\",);\n            Log::error(\"Spawn Error\", format!(\"{e:#?}\"))\n          }\n          _ => return,\n        };\n        let res = async {\n          // Nothing to do if update was never actually created,\n          // which is the case when the id is empty.\n          if update_id.is_empty() {\n            return Ok(());\n          }\n          let mut update =\n            find_one_by_id(&db_client().updates, &update_id)\n              .await\n              .context(\"failed to query to db\")?\n              .context(\"no update exists with given id\")?;\n          update.logs.push(log);\n          update.finalize();\n          update_update(update).await\n        }\n        .await;\n\n        if let Err(e) = res {\n          warn!(\n            \"failed to update update with task error log | {e:#}\"\n          );\n        }\n      }\n    });\n\n    Ok(ExecutionResult::Single(update.into()))\n  })\n}\n\n#[instrument(\n  name = \"ExecuteRequest\",\n  skip(user, update),\n  fields(\n    user_id = user.id,\n    update_id = update.id,\n    request = format!(\"{:?}\", request.extract_variant()))\n  )\n]\nasync fn task(\n  req_id: Uuid,\n  request: ExecuteRequest,\n  user: User,\n  update: Update,\n) -> anyhow::Result<String> {\n  info!(\"/execute request {req_id} | user: {}\", user.username);\n  let timer = Instant::now();\n\n  let res = match request.resolve(&ExecuteArgs { user, update }).await\n  {\n    Err(e) => Err(e.error),\n    Ok(JsonString::Err(e)) => Err(\n      anyhow::Error::from(e).context(\"failed to serialize response\"),\n    ),\n    Ok(JsonString::Ok(res)) => Ok(res),\n  };\n\n  if let Err(e) = &res {\n    warn!(\"/execute request {req_id} error: {e:#}\");\n  }\n\n  let elapsed = timer.elapsed();\n  debug!(\"/execute request {req_id} | resolve time: {elapsed:?}\");\n\n  res\n}\n\ntrait BatchExecute {\n  type Resource: KomodoResource;\n  fn single_request(name: String) -> ExecuteRequest;\n}\n\nasync fn batch_execute<E: BatchExecute>(\n  pattern: &str,\n  user: &User,\n) -> anyhow::Result<BatchExecutionResponse> {\n  let resources = list_full_for_user_using_pattern::<E::Resource>(\n    pattern,\n    Default::default(),\n    user,\n    PermissionLevel::Execute.into(),\n    &[],\n  )\n  .await?;\n  let futures = resources.into_iter().map(|resource| {\n    let user = user.clone();\n    async move {\n      inner_handler(E::single_request(resource.name.clone()), user)\n        .await\n        .map(|r| {\n          let ExecutionResult::Single(update) = r else {\n            unreachable!()\n          };\n          update\n        })\n        .map_err(|e| BatchExecutionResponseItemErr {\n          name: resource.name,\n          error: e.into(),\n        })\n        .into()\n    }\n  });\n  Ok(join_all(futures).await)\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/procedure.rs",
    "content": "use std::pin::Pin;\n\nuse database::mungos::{\n  by_id::update_one_by_id, mongodb::bson::to_document,\n};\nuse formatting::{Color, bold, colored, format_serror, muted};\nuse komodo_client::{\n  api::execute::{\n    BatchExecutionResponse, BatchRunProcedure, RunProcedure,\n  },\n  entities::{\n    alert::{Alert, AlertData, SeverityLevel},\n    komodo_timestamp,\n    permission::PermissionLevel,\n    procedure::Procedure,\n    update::Update,\n    user::User,\n  },\n};\nuse resolver_api::Resolve;\nuse tokio::sync::Mutex;\n\nuse crate::{\n  alert::send_alerts,\n  helpers::{procedure::execute_procedure, update::update_update},\n  permission::get_check_permissions,\n  resource::refresh_procedure_state_cache,\n  state::{action_states, db_client},\n};\n\nuse super::{ExecuteArgs, ExecuteRequest};\n\nimpl super::BatchExecute for BatchRunProcedure {\n  type Resource = Procedure;\n  fn single_request(procedure: String) -> ExecuteRequest {\n    ExecuteRequest::RunProcedure(RunProcedure { procedure })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchRunProcedure {\n  #[instrument(name = \"BatchRunProcedure\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchRunProcedure>(&self.pattern, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<ExecuteArgs> for RunProcedure {\n  #[instrument(name = \"RunProcedure\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    Ok(\n      resolve_inner(self.procedure, user.clone(), update.clone())\n        .await?,\n    )\n  }\n}\n\nfn resolve_inner(\n  procedure: String,\n  user: User,\n  mut update: Update,\n) -> Pin<\n  Box<\n    dyn std::future::Future<Output = anyhow::Result<Update>> + Send,\n  >,\n> {\n  Box::pin(async move {\n    let procedure = get_check_permissions::<Procedure>(\n      &procedure,\n      &user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // Need to push the initial log, as execute_procedure\n    // assumes first log is already created\n    // and will panic otherwise.\n    update.push_simple_log(\n      \"Execute procedure\",\n      format!(\n        \"{}: executing procedure '{}'\",\n        muted(\"INFO\"),\n        bold(&procedure.name)\n      ),\n    );\n\n    // get the action state for the procedure (or insert default).\n    let action_state = action_states()\n      .procedure\n      .get_or_insert_default(&procedure.id)\n      .await;\n\n    // This will set action state back to default when dropped.\n    // Will also check to ensure procedure not already busy before updating.\n    let _action_guard =\n      action_state.update(|state| state.running = true)?;\n\n    update_update(update.clone()).await?;\n\n    let update = Mutex::new(update);\n\n    let res = execute_procedure(&procedure, &update).await;\n\n    let mut update = update.into_inner();\n\n    match res {\n      Ok(_) => {\n        update.push_simple_log(\n          \"Execution ok\",\n          format!(\n            \"{}: The procedure has {} with no errors\",\n            muted(\"INFO\"),\n            colored(\"completed\", Color::Green)\n          ),\n        );\n      }\n      Err(e) => update\n        .push_error_log(\"execution error\", format_serror(&e.into())),\n    }\n\n    update.finalize();\n\n    // Need to manually update the update before cache refresh,\n    // and before broadcast with add_update.\n    // The Err case of to_document should be unreachable,\n    // but will fail to update cache in that case.\n    if let Ok(update_doc) = to_document(&update) {\n      let _ = update_one_by_id(\n        &db_client().updates,\n        &update.id,\n        database::mungos::update::Update::Set(update_doc),\n        None,\n      )\n      .await;\n      refresh_procedure_state_cache().await;\n    }\n\n    update_update(update.clone()).await?;\n\n    if !update.success && procedure.config.failure_alert {\n      warn!(\"procedure unsuccessful, alerting...\");\n      let target = update.target.clone();\n      tokio::spawn(async move {\n        let alert = Alert {\n          id: Default::default(),\n          target,\n          ts: komodo_timestamp(),\n          resolved_ts: Some(komodo_timestamp()),\n          resolved: true,\n          level: SeverityLevel::Warning,\n          data: AlertData::ProcedureFailed {\n            id: procedure.id,\n            name: procedure.name,\n          },\n        };\n        send_alerts(&[alert]).await\n      });\n    }\n\n    Ok(update)\n  })\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/repo.rs",
    "content": "use std::{collections::HashSet, future::IntoFuture, time::Duration};\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::update_one_by_id,\n  mongodb::{\n    bson::{doc, to_document},\n    options::FindOneOptions,\n  },\n};\nuse formatting::format_serror;\nuse interpolate::Interpolator;\nuse komodo_client::{\n  api::{execute::*, write::RefreshRepoCache},\n  entities::{\n    alert::{Alert, AlertData, SeverityLevel},\n    builder::{Builder, BuilderConfig},\n    komodo_timestamp,\n    permission::PermissionLevel,\n    repo::Repo,\n    server::Server,\n    update::{Log, Update},\n  },\n};\nuse periphery_client::api;\nuse resolver_api::Resolve;\nuse tokio_util::sync::CancellationToken;\n\nuse crate::{\n  alert::send_alerts,\n  api::write::WriteArgs,\n  helpers::{\n    builder::{cleanup_builder_instance, get_builder_periphery},\n    channel::repo_cancel_channel,\n    git_token, periphery_client,\n    query::{VariablesAndSecrets, get_variables_and_secrets},\n    update::update_update,\n  },\n  permission::get_check_permissions,\n  resource::{self, refresh_repo_state_cache},\n  state::{action_states, db_client},\n};\n\nuse super::{ExecuteArgs, ExecuteRequest};\n\nimpl super::BatchExecute for BatchCloneRepo {\n  type Resource = Repo;\n  fn single_request(repo: String) -> ExecuteRequest {\n    ExecuteRequest::CloneRepo(CloneRepo { repo })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchCloneRepo {\n  #[instrument(name = \"BatchCloneRepo\", skip( user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchCloneRepo>(&self.pattern, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<ExecuteArgs> for CloneRepo {\n  #[instrument(name = \"CloneRepo\", skip( user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let mut repo = get_check_permissions::<Repo>(\n      &self.repo,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the repo (or insert default).\n    let action_state =\n      action_states().repo.get_or_insert_default(&repo.id).await;\n\n    // This will set action state back to default when dropped.\n    // Will also check to ensure repo not already busy before updating.\n    let _action_guard =\n      action_state.update(|state| state.cloning = true)?;\n\n    let mut update = update.clone();\n    update_update(update.clone()).await?;\n\n    if repo.config.server_id.is_empty() {\n      return Err(anyhow!(\"repo has no server attached\").into());\n    }\n\n    let git_token = git_token(\n      &repo.config.git_provider,\n      &repo.config.git_account,\n      |https| repo.config.git_https = https,\n    )\n    .await\n    .with_context(\n      || format!(\"Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}\", repo.config.git_provider, repo.config.git_account),\n    )?;\n\n    let server =\n      resource::get::<Server>(&repo.config.server_id).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    // interpolate variables / secrets, returning the sanitizing replacers to send to\n    // periphery so it may sanitize the final command for safe logging (avoids exposing secret values)\n    let secret_replacers =\n      interpolate(&mut repo, &mut update).await?;\n\n    let logs = match periphery\n      .request(api::git::CloneRepo {\n        args: (&repo).into(),\n        git_token,\n        environment: repo.config.env_vars()?,\n        env_file_path: repo.config.env_file_path,\n        on_clone: repo.config.on_clone.into(),\n        on_pull: repo.config.on_pull.into(),\n        skip_secret_interp: repo.config.skip_secret_interp,\n        replacers: secret_replacers.into_iter().collect(),\n      })\n      .await\n    {\n      Ok(res) => res.res.logs,\n      Err(e) => {\n        vec![Log::error(\n          \"Clone Repo\",\n          format_serror(&e.context(\"Failed to clone repo\").into()),\n        )]\n      }\n    };\n\n    update.logs.extend(logs);\n    update.finalize();\n\n    if update.success {\n      update_last_pulled_time(&repo.name).await;\n    }\n\n    if let Err(e) = (RefreshRepoCache { repo: repo.id })\n      .resolve(&WriteArgs { user: user.clone() })\n      .await\n      .map_err(|e| e.error)\n      .context(\"Failed to refresh repo cache\")\n    {\n      update.push_error_log(\n        \"Refresh Repo cache\",\n        format_serror(&e.into()),\n      );\n    };\n\n    handle_repo_update_return(update).await\n  }\n}\n\nimpl super::BatchExecute for BatchPullRepo {\n  type Resource = Repo;\n  fn single_request(repo: String) -> ExecuteRequest {\n    ExecuteRequest::PullRepo(PullRepo { repo })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchPullRepo {\n  #[instrument(name = \"BatchPullRepo\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchPullRepo>(&self.pattern, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PullRepo {\n  #[instrument(name = \"PullRepo\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let mut repo = get_check_permissions::<Repo>(\n      &self.repo,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the repo (or insert default).\n    let action_state =\n      action_states().repo.get_or_insert_default(&repo.id).await;\n\n    // This will set action state back to default when dropped.\n    // Will also check to ensure repo not already busy before updating.\n    let _action_guard =\n      action_state.update(|state| state.pulling = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    if repo.config.server_id.is_empty() {\n      return Err(anyhow!(\"repo has no server attached\").into());\n    }\n\n    let git_token = git_token(\n      &repo.config.git_provider,\n      &repo.config.git_account,\n      |https| repo.config.git_https = https,\n    )\n    .await\n    .with_context(\n      || format!(\"Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}\", repo.config.git_provider, repo.config.git_account),\n    )?;\n\n    let server =\n      resource::get::<Server>(&repo.config.server_id).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    // interpolate variables / secrets, returning the sanitizing replacers to send to\n    // periphery so it may sanitize the final command for safe logging (avoids exposing secret values)\n    let secret_replacers =\n      interpolate(&mut repo, &mut update).await?;\n\n    let logs = match periphery\n      .request(api::git::PullRepo {\n        args: (&repo).into(),\n        git_token,\n        environment: repo.config.env_vars()?,\n        env_file_path: repo.config.env_file_path,\n        on_pull: repo.config.on_pull.into(),\n        skip_secret_interp: repo.config.skip_secret_interp,\n        replacers: secret_replacers.into_iter().collect(),\n      })\n      .await\n    {\n      Ok(res) => {\n        update.commit_hash = res.res.commit_hash.unwrap_or_default();\n        res.res.logs\n      }\n      Err(e) => {\n        vec![Log::error(\n          \"pull repo\",\n          format_serror(&e.context(\"failed to pull repo\").into()),\n        )]\n      }\n    };\n\n    update.logs.extend(logs);\n\n    update.finalize();\n\n    if update.success {\n      update_last_pulled_time(&repo.name).await;\n    }\n\n    if let Err(e) = (RefreshRepoCache { repo: repo.id })\n      .resolve(&WriteArgs { user: user.clone() })\n      .await\n      .map_err(|e| e.error)\n      .context(\"Failed to refresh repo cache\")\n    {\n      update.push_error_log(\n        \"Refresh Repo cache\",\n        format_serror(&e.into()),\n      );\n    };\n\n    handle_repo_update_return(update).await\n  }\n}\n\n#[instrument(skip_all, fields(update_id = update.id))]\nasync fn handle_repo_update_return(\n  update: Update,\n) -> serror::Result<Update> {\n  // Need to manually update the update before cache refresh,\n  // and before broadcast with add_update.\n  // The Err case of to_document should be unreachable,\n  // but will fail to update cache in that case.\n  if let Ok(update_doc) = to_document(&update) {\n    let _ = update_one_by_id(\n      &db_client().updates,\n      &update.id,\n      database::mungos::update::Update::Set(update_doc),\n      None,\n    )\n    .await;\n    refresh_repo_state_cache().await;\n  }\n  update_update(update.clone()).await?;\n  Ok(update)\n}\n\n#[instrument]\nasync fn update_last_pulled_time(repo_name: &str) {\n  let res = db_client()\n    .repos\n    .update_one(\n      doc! { \"name\": repo_name },\n      doc! { \"$set\": { \"info.last_pulled_at\": komodo_timestamp() } },\n    )\n    .await;\n  if let Err(e) = res {\n    warn!(\n      \"failed to update repo last_pulled_at | repo: {repo_name} | {e:#}\",\n    );\n  }\n}\n\nimpl super::BatchExecute for BatchBuildRepo {\n  type Resource = Repo;\n  fn single_request(repo: String) -> ExecuteRequest {\n    ExecuteRequest::CloneRepo(CloneRepo { repo })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchBuildRepo {\n  #[instrument(name = \"BatchBuildRepo\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchBuildRepo>(&self.pattern, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BuildRepo {\n  #[instrument(name = \"BuildRepo\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let mut repo = get_check_permissions::<Repo>(\n      &self.repo,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    if repo.config.builder_id.is_empty() {\n      return Err(anyhow!(\"Must attach builder to BuildRepo\").into());\n    }\n\n    // get the action state for the repo (or insert default).\n    let action_state =\n      action_states().repo.get_or_insert_default(&repo.id).await;\n\n    // This will set action state back to default when dropped.\n    // Will also check to ensure repo not already busy before updating.\n    let _action_guard =\n      action_state.update(|state| state.building = true)?;\n\n    let mut update = update.clone();\n    update_update(update.clone()).await?;\n\n    let git_token = git_token(\n      &repo.config.git_provider,\n      &repo.config.git_account,\n      |https| repo.config.git_https = https,\n    )\n    .await\n    .with_context(\n      || format!(\"Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}\", repo.config.git_provider, repo.config.git_account),\n    )?;\n\n    let cancel = CancellationToken::new();\n    let cancel_clone = cancel.clone();\n    let mut cancel_recv =\n      repo_cancel_channel().receiver.resubscribe();\n    let repo_id = repo.id.clone();\n\n    let builder =\n      resource::get::<Builder>(&repo.config.builder_id).await?;\n\n    let is_server_builder =\n      matches!(&builder.config, BuilderConfig::Server(_));\n\n    tokio::spawn(async move {\n      let poll = async {\n        loop {\n          let (incoming_repo_id, mut update) = tokio::select! {\n            _ = cancel_clone.cancelled() => return Ok(()),\n            id = cancel_recv.recv() => id?\n          };\n          if incoming_repo_id == repo_id {\n            if is_server_builder {\n              update.push_error_log(\"Cancel acknowledged\", \"Repo Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature.\");\n            } else {\n              update.push_simple_log(\"Cancel acknowledged\", \"The repo build cancellation has been queued, it may still take some time.\");\n            }\n            update.finalize();\n            let id = update.id.clone();\n            if let Err(e) = update_update(update).await {\n              warn!(\"failed to modify Update {id} on db | {e:#}\");\n            }\n            if !is_server_builder {\n              cancel_clone.cancel();\n            }\n            return Ok(());\n          }\n        }\n        #[allow(unreachable_code)]\n        anyhow::Ok(())\n      };\n      tokio::select! {\n        _ = cancel_clone.cancelled() => {}\n        _ = poll => {}\n      }\n    });\n\n    // GET BUILDER PERIPHERY\n\n    let (periphery, cleanup_data) = match get_builder_periphery(\n      repo.name.clone(),\n      None,\n      builder,\n      &mut update,\n    )\n    .await\n    {\n      Ok(builder) => builder,\n      Err(e) => {\n        warn!(\"failed to get builder for repo {} | {e:#}\", repo.name);\n        update.logs.push(Log::error(\n          \"get builder\",\n          format_serror(&e.context(\"failed to get builder\").into()),\n        ));\n        return handle_builder_early_return(\n          update, repo.id, repo.name, false,\n        )\n        .await;\n      }\n    };\n\n    // CLONE REPO\n\n    // interpolate variables / secrets, returning the sanitizing replacers to send to\n    // periphery so it may sanitize the final command for safe logging (avoids exposing secret values)\n    let secret_replacers =\n      interpolate(&mut repo, &mut update).await?;\n\n    let res = tokio::select! {\n      res = periphery\n        .request(api::git::CloneRepo {\n          args: (&repo).into(),\n          git_token,\n          environment: repo.config.env_vars()?,\n          env_file_path: repo.config.env_file_path,\n          on_clone: repo.config.on_clone.into(),\n          on_pull: repo.config.on_pull.into(),\n          skip_secret_interp: repo.config.skip_secret_interp,\n          replacers: secret_replacers.into_iter().collect()\n        }) => res,\n      _ = cancel.cancelled() => {\n        debug!(\"build cancelled during clone, cleaning up builder\");\n        update.push_error_log(\"build cancelled\", String::from(\"user cancelled build during repo clone\"));\n        cleanup_builder_instance(cleanup_data, &mut update)\n          .await;\n        info!(\"builder cleaned up\");\n        return handle_builder_early_return(update, repo.id, repo.name, true).await\n      },\n    };\n\n    let commit_message = match res {\n      Ok(res) => {\n        debug!(\"finished repo clone\");\n        update.logs.extend(res.res.logs);\n        update.commit_hash = res.res.commit_hash.unwrap_or_default();\n\n        res.res.commit_message.unwrap_or_default()\n      }\n      Err(e) => {\n        update.push_error_log(\n          \"Clone Repo\",\n          format_serror(&e.context(\"Failed to clone repo\").into()),\n        );\n        Default::default()\n      }\n    };\n\n    update.finalize();\n\n    let db = db_client();\n\n    if update.success {\n      let _ = db\n        .repos\n        .update_one(\n          doc! { \"name\": &repo.name },\n          doc! { \"$set\": {\n            \"info.last_built_at\": komodo_timestamp(),\n            \"info.built_hash\": &update.commit_hash,\n            \"info.built_message\": commit_message\n          }},\n        )\n        .await;\n    }\n\n    // stop the cancel listening task from going forever\n    cancel.cancel();\n\n    // If building on temporary cloud server (AWS),\n    // this will terminate the server.\n    cleanup_builder_instance(cleanup_data, &mut update).await;\n\n    // Need to manually update the update before cache refresh,\n    // and before broadcast with add_update.\n    // The Err case of to_document should be unreachable,\n    // but will fail to update cache in that case.\n    if let Ok(update_doc) = to_document(&update) {\n      let _ = update_one_by_id(\n        &db.updates,\n        &update.id,\n        database::mungos::update::Update::Set(update_doc),\n        None,\n      )\n      .await;\n      refresh_repo_state_cache().await;\n    }\n\n    update_update(update.clone()).await?;\n\n    if !update.success {\n      warn!(\"repo build unsuccessful, alerting...\");\n      let target = update.target.clone();\n      tokio::spawn(async move {\n        let alert = Alert {\n          id: Default::default(),\n          target,\n          ts: komodo_timestamp(),\n          resolved_ts: Some(komodo_timestamp()),\n          resolved: true,\n          level: SeverityLevel::Warning,\n          data: AlertData::RepoBuildFailed {\n            id: repo.id,\n            name: repo.name,\n          },\n        };\n        send_alerts(&[alert]).await\n      });\n    }\n\n    Ok(update)\n  }\n}\n\n#[instrument(skip(update))]\nasync fn handle_builder_early_return(\n  mut update: Update,\n  repo_id: String,\n  repo_name: String,\n  is_cancel: bool,\n) -> serror::Result<Update> {\n  update.finalize();\n  // Need to manually update the update before cache refresh,\n  // and before broadcast with add_update.\n  // The Err case of to_document should be unreachable,\n  // but will fail to update cache in that case.\n  if let Ok(update_doc) = to_document(&update) {\n    let _ = update_one_by_id(\n      &db_client().updates,\n      &update.id,\n      database::mungos::update::Update::Set(update_doc),\n      None,\n    )\n    .await;\n    refresh_repo_state_cache().await;\n  }\n  update_update(update.clone()).await?;\n  if !update.success && !is_cancel {\n    warn!(\"repo build unsuccessful, alerting...\");\n    let target = update.target.clone();\n    tokio::spawn(async move {\n      let alert = Alert {\n        id: Default::default(),\n        target,\n        ts: komodo_timestamp(),\n        resolved_ts: Some(komodo_timestamp()),\n        resolved: true,\n        level: SeverityLevel::Warning,\n        data: AlertData::RepoBuildFailed {\n          id: repo_id,\n          name: repo_name,\n        },\n      };\n      send_alerts(&[alert]).await\n    });\n  }\n  Ok(update)\n}\n\n#[instrument(skip_all)]\npub async fn validate_cancel_repo_build(\n  request: &ExecuteRequest,\n) -> anyhow::Result<()> {\n  if let ExecuteRequest::CancelRepoBuild(req) = request {\n    let repo = resource::get::<Repo>(&req.repo).await?;\n\n    let db = db_client();\n\n    let (latest_build, latest_cancel) = tokio::try_join!(\n      db.updates\n        .find_one(doc! {\n          \"operation\": \"BuildRepo\",\n          \"target.id\": &repo.id,\n        },)\n        .with_options(\n          FindOneOptions::builder()\n            .sort(doc! { \"start_ts\": -1 })\n            .build()\n        )\n        .into_future(),\n      db.updates\n        .find_one(doc! {\n          \"operation\": \"CancelRepoBuild\",\n          \"target.id\": &repo.id,\n        },)\n        .with_options(\n          FindOneOptions::builder()\n            .sort(doc! { \"start_ts\": -1 })\n            .build()\n        )\n        .into_future()\n    )?;\n\n    match (latest_build, latest_cancel) {\n      (Some(build), Some(cancel)) => {\n        if cancel.start_ts > build.start_ts {\n          return Err(anyhow!(\n            \"Repo build has already been cancelled\"\n          ));\n        }\n      }\n      (None, _) => return Err(anyhow!(\"No repo build in progress\")),\n      _ => {}\n    };\n  }\n  Ok(())\n}\n\nimpl Resolve<ExecuteArgs> for CancelRepoBuild {\n  #[instrument(name = \"CancelRepoBuild\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let repo = get_check_permissions::<Repo>(\n      &self.repo,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // make sure the build is building\n    if !action_states()\n      .repo\n      .get(&repo.id)\n      .await\n      .and_then(|s| s.get().ok().map(|s| s.building))\n      .unwrap_or_default()\n    {\n      return Err(anyhow!(\"Repo is not building.\").into());\n    }\n\n    let mut update = update.clone();\n\n    update.push_simple_log(\n      \"cancel triggered\",\n      \"the repo build cancel has been triggered\",\n    );\n    update_update(update.clone()).await?;\n\n    repo_cancel_channel()\n      .sender\n      .lock()\n      .await\n      .send((repo.id, update.clone()))?;\n\n    // Make sure cancel is set to complete after some time in case\n    // no reciever is there to do it. Prevents update stuck in InProgress.\n    let update_id = update.id.clone();\n    tokio::spawn(async move {\n      tokio::time::sleep(Duration::from_secs(60)).await;\n      if let Err(e) = update_one_by_id(\n        &db_client().updates,\n        &update_id,\n        doc! { \"$set\": { \"status\": \"Complete\" } },\n        None,\n      )\n      .await\n      {\n        warn!(\n          \"failed to set CancelRepoBuild Update status Complete after timeout | {e:#}\"\n        )\n      }\n    });\n\n    Ok(update)\n  }\n}\n\nasync fn interpolate(\n  repo: &mut Repo,\n  update: &mut Update,\n) -> anyhow::Result<HashSet<(String, String)>> {\n  if !repo.config.skip_secret_interp {\n    let VariablesAndSecrets { variables, secrets } =\n      get_variables_and_secrets().await?;\n\n    let mut interpolator =\n      Interpolator::new(Some(&variables), &secrets);\n\n    interpolator\n      .interpolate_repo(repo)?\n      .push_logs(&mut update.logs);\n\n    Ok(interpolator.secret_replacers)\n  } else {\n    Ok(Default::default())\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/server.rs",
    "content": "use anyhow::Context;\nuse formatting::format_serror;\nuse komodo_client::{\n  api::execute::*,\n  entities::{\n    all_logs_success,\n    permission::PermissionLevel,\n    server::Server,\n    update::{Log, Update},\n  },\n};\nuse periphery_client::api;\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::{periphery_client, update::update_update},\n  monitor::update_cache_for_server,\n  permission::get_check_permissions,\n  state::action_states,\n};\n\nuse super::ExecuteArgs;\n\nimpl Resolve<ExecuteArgs> for StartContainer {\n  #[instrument(name = \"StartContainer\", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure deployment not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard = action_state\n      .update(|state| state.starting_containers = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::container::StartContainer {\n        name: self.container,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"start container\",\n        format_serror(&e.context(\"failed to start container\").into()),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for RestartContainer {\n  #[instrument(name = \"RestartContainer\", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the deployment (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard = action_state\n      .update(|state| state.restarting_containers = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::container::RestartContainer {\n        name: self.container,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"restart container\",\n        format_serror(\n          &e.context(\"failed to restart container\").into(),\n        ),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PauseContainer {\n  #[instrument(name = \"PauseContainer\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pausing_containers = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::container::PauseContainer {\n        name: self.container,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"pause container\",\n        format_serror(&e.context(\"failed to pause container\").into()),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for UnpauseContainer {\n  #[instrument(name = \"UnpauseContainer\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard = action_state\n      .update(|state| state.unpausing_containers = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::container::UnpauseContainer {\n        name: self.container,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"unpause container\",\n        format_serror(\n          &e.context(\"failed to unpause container\").into(),\n        ),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for StopContainer {\n  #[instrument(name = \"StopContainer\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard = action_state\n      .update(|state| state.stopping_containers = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::container::StopContainer {\n        name: self.container,\n        signal: self.signal,\n        time: self.time,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"stop container\",\n        format_serror(&e.context(\"failed to stop container\").into()),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for DestroyContainer {\n  #[instrument(name = \"DestroyContainer\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let DestroyContainer {\n      server,\n      container,\n      signal,\n      time,\n    } = self;\n    let server = get_check_permissions::<Server>(\n      &server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pruning_containers = true)?;\n\n    let mut update = update.clone();\n\n    // Send update after setting action state, this way frontend gets correct state.\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::container::RemoveContainer {\n        name: container,\n        signal,\n        time,\n      })\n      .await\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"stop container\",\n        format_serror(&e.context(\"failed to stop container\").into()),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for StartAllContainers {\n  #[instrument(name = \"StartAllContainers\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard = action_state\n      .update(|state| state.starting_containers = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let logs = periphery_client(&server)?\n      .request(api::container::StartAllContainers {})\n      .await\n      .context(\"failed to start all containers on host\")?;\n\n    update.logs.extend(logs);\n\n    if all_logs_success(&update.logs) {\n      update.push_simple_log(\n        \"start all containers\",\n        String::from(\"All containers have been started on the host.\"),\n      );\n    }\n\n    update_cache_for_server(&server, true).await;\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for RestartAllContainers {\n  #[instrument(name = \"RestartAllContainers\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard = action_state\n      .update(|state| state.restarting_containers = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let logs = periphery_client(&server)?\n      .request(api::container::RestartAllContainers {})\n      .await\n      .context(\"failed to restart all containers on host\")?;\n\n    update.logs.extend(logs);\n\n    if all_logs_success(&update.logs) {\n      update.push_simple_log(\n        \"restart all containers\",\n        String::from(\n          \"All containers have been restarted on the host.\",\n        ),\n      );\n    }\n\n    update_cache_for_server(&server, true).await;\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PauseAllContainers {\n  #[instrument(name = \"PauseAllContainers\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pausing_containers = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let logs = periphery_client(&server)?\n      .request(api::container::PauseAllContainers {})\n      .await\n      .context(\"failed to pause all containers on host\")?;\n\n    update.logs.extend(logs);\n\n    if all_logs_success(&update.logs) {\n      update.push_simple_log(\n        \"pause all containers\",\n        String::from(\"All containers have been paused on the host.\"),\n      );\n    }\n\n    update_cache_for_server(&server, true).await;\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for UnpauseAllContainers {\n  #[instrument(name = \"UnpauseAllContainers\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard = action_state\n      .update(|state| state.unpausing_containers = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let logs = periphery_client(&server)?\n      .request(api::container::UnpauseAllContainers {})\n      .await\n      .context(\"failed to unpause all containers on host\")?;\n\n    update.logs.extend(logs);\n\n    if all_logs_success(&update.logs) {\n      update.push_simple_log(\n        \"unpause all containers\",\n        String::from(\n          \"All containers have been unpaused on the host.\",\n        ),\n      );\n    }\n\n    update_cache_for_server(&server, true).await;\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for StopAllContainers {\n  #[instrument(name = \"StopAllContainers\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard = action_state\n      .update(|state| state.stopping_containers = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let logs = periphery_client(&server)?\n      .request(api::container::StopAllContainers {})\n      .await\n      .context(\"failed to stop all containers on host\")?;\n\n    update.logs.extend(logs);\n\n    if all_logs_success(&update.logs) {\n      update.push_simple_log(\n        \"stop all containers\",\n        String::from(\"All containers have been stopped on the host.\"),\n      );\n    }\n\n    update_cache_for_server(&server, true).await;\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PruneContainers {\n  #[instrument(name = \"PruneContainers\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pruning_containers = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::container::PruneContainers {})\n      .await\n      .context(format!(\n        \"failed to prune containers on server {}\",\n        server.name\n      )) {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"prune containers\",\n        format_serror(\n          &e.context(\"failed to prune containers\").into(),\n        ),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for DeleteNetwork {\n  #[instrument(name = \"DeleteNetwork\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::network::DeleteNetwork {\n        name: self.name.clone(),\n      })\n      .await\n      .context(format!(\n        \"failed to delete network {} on server {}\",\n        self.name, server.name\n      )) {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"delete network\",\n        format_serror(\n          &e.context(format!(\n            \"failed to delete network {}\",\n            self.name\n          ))\n          .into(),\n        ),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PruneNetworks {\n  #[instrument(name = \"PruneNetworks\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pruning_networks = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::network::PruneNetworks {})\n      .await\n      .context(format!(\n        \"failed to prune networks on server {}\",\n        server.name\n      )) {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"prune networks\",\n        format_serror(&e.context(\"failed to prune networks\").into()),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for DeleteImage {\n  #[instrument(name = \"DeleteImage\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::image::DeleteImage {\n        name: self.name.clone(),\n      })\n      .await\n      .context(format!(\n        \"failed to delete image {} on server {}\",\n        self.name, server.name\n      )) {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"delete image\",\n        format_serror(\n          &e.context(format!(\"failed to delete image {}\", self.name))\n            .into(),\n        ),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PruneImages {\n  #[instrument(name = \"PruneImages\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pruning_images = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log =\n      match periphery.request(api::image::PruneImages {}).await {\n        Ok(log) => log,\n        Err(e) => Log::error(\n          \"prune images\",\n          format!(\n            \"failed to prune images on server {} | {e:#?}\",\n            server.name\n          ),\n        ),\n      };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for DeleteVolume {\n  #[instrument(name = \"DeleteVolume\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery\n      .request(api::volume::DeleteVolume {\n        name: self.name.clone(),\n      })\n      .await\n      .context(format!(\n        \"failed to delete volume {} on server {}\",\n        self.name, server.name\n      )) {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"delete volume\",\n        format_serror(\n          &e.context(format!(\n            \"failed to delete volume {}\",\n            self.name\n          ))\n          .into(),\n        ),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PruneVolumes {\n  #[instrument(name = \"PruneVolumes\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pruning_volumes = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log =\n      match periphery.request(api::volume::PruneVolumes {}).await {\n        Ok(log) => log,\n        Err(e) => Log::error(\n          \"prune volumes\",\n          format!(\n            \"failed to prune volumes on server {} | {e:#?}\",\n            server.name\n          ),\n        ),\n      };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PruneDockerBuilders {\n  #[instrument(name = \"PruneDockerBuilders\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pruning_builders = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log =\n      match periphery.request(api::build::PruneBuilders {}).await {\n        Ok(log) => log,\n        Err(e) => Log::error(\n          \"prune builders\",\n          format!(\n            \"failed to docker builder prune on server {} | {e:#?}\",\n            server.name\n          ),\n        ),\n      };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PruneBuildx {\n  #[instrument(name = \"PruneBuildx\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pruning_buildx = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log =\n      match periphery.request(api::build::PruneBuildx {}).await {\n        Ok(log) => log,\n        Err(e) => Log::error(\n          \"prune buildx\",\n          format!(\n            \"failed to docker buildx prune on server {} | {e:#?}\",\n            server.name\n          ),\n        ),\n      };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PruneSystem {\n  #[instrument(name = \"PruneSystem\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    // get the action state for the server (or insert default).\n    let action_state = action_states()\n      .server\n      .get_or_insert_default(&server.id)\n      .await;\n\n    // Will check to ensure server not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pruning_system = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let log = match periphery.request(api::PruneSystem {}).await {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"prune system\",\n        format!(\n          \"failed to docker system prune on server {} | {e:#?}\",\n          server.name\n        ),\n      ),\n    };\n\n    update.logs.push(log);\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/stack.rs",
    "content": "use std::{collections::HashSet, str::FromStr};\n\nuse anyhow::Context;\nuse database::mungos::mongodb::bson::{\n  doc, oid::ObjectId, to_bson, to_document,\n};\nuse formatting::format_serror;\nuse interpolate::Interpolator;\nuse komodo_client::{\n  api::{execute::*, write::RefreshStackCache},\n  entities::{\n    FileContents,\n    permission::PermissionLevel,\n    repo::Repo,\n    server::Server,\n    stack::{\n      Stack, StackFileRequires, StackInfo, StackRemoteFileContents,\n    },\n    update::{Log, Update},\n    user::User,\n  },\n};\nuse periphery_client::api::compose::*;\nuse resolver_api::Resolve;\n\nuse crate::{\n  api::write::WriteArgs,\n  helpers::{\n    periphery_client,\n    query::{VariablesAndSecrets, get_variables_and_secrets},\n    stack_git_token,\n    update::{\n      add_update_without_send, init_execution_update, update_update,\n    },\n  },\n  monitor::update_cache_for_server,\n  permission::get_check_permissions,\n  resource,\n  stack::{execute::execute_compose, get_stack_and_server},\n  state::{action_states, db_client},\n};\n\nuse super::{ExecuteArgs, ExecuteRequest};\n\nimpl super::BatchExecute for BatchDeployStack {\n  type Resource = Stack;\n  fn single_request(stack: String) -> ExecuteRequest {\n    ExecuteRequest::DeployStack(DeployStack {\n      stack,\n      services: Vec::new(),\n      stop_time: None,\n    })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchDeployStack {\n  #[instrument(name = \"BatchDeployStack\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchDeployStack>(&self.pattern, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<ExecuteArgs> for DeployStack {\n  #[instrument(name = \"DeployStack\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (mut stack, server) = get_stack_and_server(\n      &self.stack,\n      user,\n      PermissionLevel::Execute.into(),\n      true,\n    )\n    .await?;\n\n    let mut repo = if !stack.config.files_on_host\n      && !stack.config.linked_repo.is_empty()\n    {\n      crate::resource::get::<Repo>(&stack.config.linked_repo)\n        .await?\n        .into()\n    } else {\n      None\n    };\n\n    // get the action state for the stack (or insert default).\n    let action_state =\n      action_states().stack.get_or_insert_default(&stack.id).await;\n\n    // Will check to ensure stack not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.deploying = true)?;\n\n    let mut update = update.clone();\n\n    update_update(update.clone()).await?;\n\n    if !self.services.is_empty() {\n      update.logs.push(Log::simple(\n        \"Service/s\",\n        format!(\n          \"Execution requested for Stack service/s {}\",\n          self.services.join(\", \")\n        ),\n      ))\n    }\n\n    let git_token =\n      stack_git_token(&mut stack, repo.as_mut()).await?;\n\n    let registry_token = crate::helpers::registry_token(\n      &stack.config.registry_provider,\n      &stack.config.registry_account,\n    ).await.with_context(\n      || format!(\"Failed to get registry token in call to db. Stopping run. | {} | {}\", stack.config.registry_provider, stack.config.registry_account),\n    )?;\n\n    // interpolate variables / secrets, returning the sanitizing replacers to send to\n    // periphery so it may sanitize the final command for safe logging (avoids exposing secret values)\n    let secret_replacers = if !stack.config.skip_secret_interp {\n      let VariablesAndSecrets { variables, secrets } =\n        get_variables_and_secrets().await?;\n\n      let mut interpolator =\n        Interpolator::new(Some(&variables), &secrets);\n\n      interpolator.interpolate_stack(&mut stack)?;\n      if let Some(repo) = repo.as_mut()\n        && !repo.config.skip_secret_interp\n      {\n        interpolator.interpolate_repo(repo)?;\n      }\n      interpolator.push_logs(&mut update.logs);\n\n      interpolator.secret_replacers\n    } else {\n      Default::default()\n    };\n\n    let ComposeUpResponse {\n      logs,\n      deployed,\n      services,\n      file_contents,\n      missing_files,\n      remote_errors,\n      compose_config,\n      commit_hash,\n      commit_message,\n    } = periphery_client(&server)?\n      .request(ComposeUp {\n        stack: stack.clone(),\n        services: self.services,\n        repo,\n        git_token,\n        registry_token,\n        replacers: secret_replacers.into_iter().collect(),\n      })\n      .await?;\n\n    update.logs.extend(logs);\n\n    let update_info = async {\n      let latest_services = if services.is_empty() {\n        // maybe better to do something else here for services.\n        stack.info.latest_services.clone()\n      } else {\n        services\n      };\n\n      // This ensures to get the latest project name,\n      // as it may have changed since the last deploy.\n      let project_name = stack.project_name(true);\n\n      let (\n        deployed_services,\n        deployed_contents,\n        deployed_config,\n        deployed_hash,\n        deployed_message,\n      ) = if deployed {\n        (\n          Some(latest_services.clone()),\n          Some(\n            file_contents\n              .iter()\n              .map(|f| FileContents {\n                path: f.path.clone(),\n                contents: f.contents.clone(),\n              })\n              .collect(),\n          ),\n          compose_config,\n          commit_hash.clone(),\n          commit_message.clone(),\n        )\n      } else {\n        (\n          stack.info.deployed_services,\n          stack.info.deployed_contents,\n          stack.info.deployed_config,\n          stack.info.deployed_hash,\n          stack.info.deployed_message,\n        )\n      };\n\n      let info = StackInfo {\n        missing_files,\n        deployed_project_name: project_name.into(),\n        deployed_services,\n        deployed_contents,\n        deployed_config,\n        deployed_hash,\n        deployed_message,\n        latest_services,\n        remote_contents: stack\n          .config\n          .file_contents\n          .is_empty()\n          .then_some(file_contents),\n        remote_errors: stack\n          .config\n          .file_contents\n          .is_empty()\n          .then_some(remote_errors),\n        latest_hash: commit_hash,\n        latest_message: commit_message,\n      };\n\n      let info = to_document(&info)\n        .context(\"failed to serialize stack info to bson\")?;\n\n      db_client()\n        .stacks\n        .update_one(\n          doc! { \"name\": &stack.name },\n          doc! { \"$set\": { \"info\": info } },\n        )\n        .await\n        .context(\"failed to update stack info on db\")?;\n      anyhow::Ok(())\n    };\n\n    // This will be weird with single service deploys. Come back to it.\n    if let Err(e) = update_info.await {\n      update.push_error_log(\n        \"refresh stack info\",\n        format_serror(\n          &e.context(\"failed to refresh stack info on db\").into(),\n        ),\n      )\n    }\n\n    // Ensure cached stack state up to date by updating server cache\n    update_cache_for_server(&server, true).await;\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl super::BatchExecute for BatchDeployStackIfChanged {\n  type Resource = Stack;\n  fn single_request(stack: String) -> ExecuteRequest {\n    ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {\n      stack,\n      stop_time: None,\n    })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {\n  #[instrument(name = \"BatchDeployStackIfChanged\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchDeployStackIfChanged>(\n        &self.pattern,\n        user,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ExecuteArgs> for DeployStackIfChanged {\n  #[instrument(name = \"DeployStackIfChanged\", skip(user, update), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let stack = get_check_permissions::<Stack>(\n      &self.stack,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    RefreshStackCache {\n      stack: stack.id.clone(),\n    }\n    .resolve(&WriteArgs { user: user.clone() })\n    .await?;\n\n    let stack = resource::get::<Stack>(&stack.id).await?;\n\n    let action = match (\n      &stack.info.deployed_contents,\n      &stack.info.remote_contents,\n    ) {\n      (Some(deployed_contents), Some(latest_contents)) => {\n        let services = stack\n          .info\n          .latest_services\n          .iter()\n          .map(|s| s.service_name.clone())\n          .collect::<Vec<_>>();\n        resolve_deploy_if_changed_action(\n          deployed_contents,\n          latest_contents,\n          &services,\n        )\n      }\n      (None, _) => DeployIfChangedAction::FullDeploy,\n      _ => DeployIfChangedAction::Services {\n        deploy: Vec::new(),\n        restart: Vec::new(),\n      },\n    };\n\n    let mut update = update.clone();\n\n    match action {\n      // Existing path pre 1.19.1\n      DeployIfChangedAction::FullDeploy => {\n        // Don't actually send it here, let the handler send it after it can set action state.\n        // This is usually done in crate::helpers::update::init_execution_update.\n        update.id = add_update_without_send(&update).await?;\n\n        DeployStack {\n          stack: stack.name,\n          services: Vec::new(),\n          stop_time: self.stop_time,\n        }\n        .resolve(&ExecuteArgs {\n          user: user.clone(),\n          update,\n        })\n        .await\n      }\n      DeployIfChangedAction::FullRestart => {\n        // For git repo based stacks, need to do a\n        // PullStack in order to ensure latest repo contents on the\n        // host before restart.\n        maybe_pull_stack(&stack, Some(&mut update)).await?;\n\n        let mut update =\n          restart_services(stack.name, Vec::new(), user).await?;\n\n        if update.success {\n          // Need to update 'info.deployed_contents' with the\n          // latest contents so next check doesn't read the same diff.\n          update_deployed_contents_with_latest(\n            &stack.id,\n            stack.info.remote_contents,\n            &mut update,\n          )\n          .await;\n        }\n\n        Ok(update)\n      }\n      DeployIfChangedAction::Services { deploy, restart } => {\n        match (deploy.is_empty(), restart.is_empty()) {\n          // Both empty, nothing to do\n          (true, true) => {\n            update.push_simple_log(\n              \"Diff compose files\",\n              String::from(\n                \"Deploy cancelled after no changes detected.\",\n              ),\n            );\n            update.finalize();\n            Ok(update)\n          }\n          // Only restart\n          (true, false) => {\n            // For git repo based stacks, need to do a\n            // PullStack in order to ensure latest repo contents on the\n            // host before restart. Only necessary if no \"deploys\" (deploy already pulls stack).\n            maybe_pull_stack(&stack, Some(&mut update)).await?;\n\n            let mut update =\n              restart_services(stack.name, restart, user).await?;\n\n            if update.success {\n              // Need to update 'info.deployed_contents' with the\n              // latest contents so next check doesn't read the same diff.\n              update_deployed_contents_with_latest(\n                &stack.id,\n                stack.info.remote_contents,\n                &mut update,\n              )\n              .await;\n            }\n\n            Ok(update)\n          }\n          // Only deploy\n          (false, true) => {\n            deploy_services(stack.name, deploy, user).await\n          }\n          // Deploy then restart, returning non-db update with executed services.\n          (false, false) => {\n            update.push_simple_log(\n              \"Execute Deploys\",\n              format!(\"Deploying: {}\", deploy.join(\", \"),),\n            );\n            // This already updates 'stack.info.deployed_services',\n            // restart doesn't require this again.\n            let deploy_update =\n              deploy_services(stack.name.clone(), deploy, user)\n                .await?;\n            if !deploy_update.success {\n              update.push_error_log(\n                \"Execute Deploys\",\n                String::from(\"There was a failure in service deploy\"),\n              );\n              update.finalize();\n              return Ok(update);\n            }\n\n            update.push_simple_log(\n              \"Execute Restarts\",\n              format!(\"Restarting: {}\", restart.join(\", \"),),\n            );\n            let restart_update =\n              restart_services(stack.name, restart, user).await?;\n            if !restart_update.success {\n              update.push_error_log(\n                \"Execute Restarts\",\n                String::from(\n                  \"There was a failure in a service restart\",\n                ),\n              );\n            }\n\n            update.finalize();\n            Ok(update)\n          }\n        }\n      }\n    }\n  }\n}\n\nasync fn deploy_services(\n  stack: String,\n  services: Vec<String>,\n  user: &User,\n) -> serror::Result<Update> {\n  // The existing update is initialized to DeployStack,\n  // but also has not been created on database.\n  // Setup a new update here.\n  let req = ExecuteRequest::DeployStack(DeployStack {\n    stack,\n    services,\n    stop_time: None,\n  });\n  let update = init_execution_update(&req, user).await?;\n  let ExecuteRequest::DeployStack(req) = req else {\n    unreachable!()\n  };\n  req\n    .resolve(&ExecuteArgs {\n      user: user.clone(),\n      update,\n    })\n    .await\n}\n\nasync fn restart_services(\n  stack: String,\n  services: Vec<String>,\n  user: &User,\n) -> serror::Result<Update> {\n  // The existing update is initialized to DeployStack,\n  // but also has not been created on database.\n  // Setup a new update here.\n  let req =\n    ExecuteRequest::RestartStack(RestartStack { stack, services });\n  let update = init_execution_update(&req, user).await?;\n  let ExecuteRequest::RestartStack(req) = req else {\n    unreachable!()\n  };\n  req\n    .resolve(&ExecuteArgs {\n      user: user.clone(),\n      update,\n    })\n    .await\n}\n\n/// This can safely be called in [DeployStackIfChanged]\n/// when there are ONLY changes to config files requiring restart,\n/// AFTER the restart has been successfully completed.\n///\n/// In the case the if changed action is not FullDeploy,\n/// the only file diff possible is to config files.\n/// Also note either full or service deploy will already update 'deployed_contents'\n/// making this method unnecessary in those cases.\n///\n/// Changes to config files after restart is applied should\n/// be taken as the deployed contents, otherwise next changed check\n/// will restart service again for no reason.\nasync fn update_deployed_contents_with_latest(\n  id: &str,\n  contents: Option<Vec<StackRemoteFileContents>>,\n  update: &mut Update,\n) {\n  let Some(contents) = contents else {\n    return;\n  };\n  let contents = contents\n    .into_iter()\n    .map(|f| FileContents {\n      path: f.path,\n      contents: f.contents,\n    })\n    .collect::<Vec<_>>();\n  if let Err(e) = (async {\n    let contents = to_bson(&contents)\n      .context(\"Failed to serialize contents to bson\")?;\n    let id =\n      ObjectId::from_str(id).context(\"Id is not valid ObjectId\")?;\n    db_client()\n      .stacks\n      .update_one(\n        doc! { \"_id\": id },\n        doc! { \"$set\": { \"info.deployed_contents\": contents } },\n      )\n      .await\n      .context(\"Failed to update stack 'deployed_contents'\")?;\n    anyhow::Ok(())\n  })\n  .await\n  {\n    update.push_error_log(\n      \"Update content cache\",\n      format_serror(&e.into()),\n    );\n    update.finalize();\n    let _ = update_update(update.clone()).await;\n  }\n}\n\nenum DeployIfChangedAction {\n  /// Changes to any compose or env files\n  /// always lead to this.\n  FullDeploy,\n  /// If the above is not met, then changes to\n  /// any changed additional file with `requires = \"Restart\"`\n  /// and empty services array will lead to this.\n  FullRestart,\n  /// If all changed additional files have specific services\n  /// they depend on, collect the final necessary\n  /// services to deploy / restart.\n  /// If eg `deploy` is empty, no services will be redeployed, same for `restart`.\n  /// If both are empty, nothing is to be done.\n  Services {\n    deploy: Vec<String>,\n    restart: Vec<String>,\n  },\n}\n\nfn resolve_deploy_if_changed_action(\n  deployed_contents: &[FileContents],\n  latest_contents: &[StackRemoteFileContents],\n  all_services: &[String],\n) -> DeployIfChangedAction {\n  let mut full_restart = false;\n  let mut deploy = HashSet::<String>::new();\n  let mut restart = HashSet::<String>::new();\n\n  for latest in latest_contents {\n    let Some(deployed) =\n      deployed_contents.iter().find(|c| c.path == latest.path)\n    else {\n      // If file doesn't exist in deployed contents, do full\n      // deploy to align this.\n      return DeployIfChangedAction::FullDeploy;\n    };\n    // Ignore unchanged files\n    if latest.contents == deployed.contents {\n      continue;\n    }\n    match (latest.requires, latest.services.is_empty()) {\n      (StackFileRequires::Redeploy, true) => {\n        // File has requires = \"Redeploy\" at global level.\n        // Can do early return here.\n        return DeployIfChangedAction::FullDeploy;\n      }\n      (StackFileRequires::Redeploy, false) => {\n        // Requires redeploy on specific services\n        deploy.extend(latest.services.clone());\n      }\n      (StackFileRequires::Restart, true) => {\n        // Services empty -> Full restart\n        full_restart = true;\n      }\n      (StackFileRequires::Restart, false) => {\n        restart.extend(latest.services.clone());\n      }\n      (StackFileRequires::None, _) => {\n        // File can be ignored even with changes.\n        continue;\n      }\n    }\n  }\n\n  match (full_restart, deploy.is_empty()) {\n    // Full restart required with NO deploys needed -> Full Restart\n    (true, true) => DeployIfChangedAction::FullRestart,\n    // Full restart required WITH deploys needed -> Deploy those, restart all others\n    (true, false) => DeployIfChangedAction::Services {\n      restart: all_services\n        .iter()\n        // Only keep ones that don't need deploy\n        .filter(|&s| !deploy.contains(s))\n        .cloned()\n        .collect(),\n      deploy: deploy.into_iter().collect(),\n    },\n    // No full restart needed -> Deploy / restart as. pickedup.\n    (false, _) => DeployIfChangedAction::Services {\n      deploy: deploy.into_iter().collect(),\n      restart: restart.into_iter().collect(),\n    },\n  }\n}\n\nimpl super::BatchExecute for BatchPullStack {\n  type Resource = Stack;\n  fn single_request(stack: String) -> ExecuteRequest {\n    ExecuteRequest::PullStack(PullStack {\n      stack,\n      services: Vec::new(),\n    })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchPullStack {\n  #[instrument(name = \"BatchPullStack\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    Ok(\n      super::batch_execute::<BatchPullStack>(&self.pattern, user)\n        .await?,\n    )\n  }\n}\n\nasync fn maybe_pull_stack(\n  stack: &Stack,\n  update: Option<&mut Update>,\n) -> anyhow::Result<()> {\n  if stack.config.files_on_host\n    || (stack.config.repo.is_empty()\n      && stack.config.linked_repo.is_empty())\n  {\n    // Not repo based, no pull necessary\n    return Ok(());\n  }\n  let server =\n    resource::get::<Server>(&stack.config.server_id).await?;\n  let repo = if stack.config.repo.is_empty()\n    && !stack.config.linked_repo.is_empty()\n  {\n    Some(resource::get::<Repo>(&stack.config.linked_repo).await?)\n  } else {\n    None\n  };\n  pull_stack_inner(stack.clone(), Vec::new(), &server, repo, update)\n    .await?;\n  Ok(())\n}\n\npub async fn pull_stack_inner(\n  mut stack: Stack,\n  services: Vec<String>,\n  server: &Server,\n  mut repo: Option<Repo>,\n  mut update: Option<&mut Update>,\n) -> anyhow::Result<ComposePullResponse> {\n  if let Some(update) = update.as_mut()\n    && !services.is_empty()\n  {\n    update.logs.push(Log::simple(\n      \"Service/s\",\n      format!(\n        \"Execution requested for Stack service/s {}\",\n        services.join(\", \")\n      ),\n    ))\n  }\n\n  let git_token = stack_git_token(&mut stack, repo.as_mut()).await?;\n\n  let registry_token = crate::helpers::registry_token(\n      &stack.config.registry_provider,\n      &stack.config.registry_account,\n    ).await.with_context(\n      || format!(\"Failed to get registry token in call to db. Stopping run. | {} | {}\", stack.config.registry_provider, stack.config.registry_account),\n    )?;\n\n  // interpolate variables / secrets\n  let secret_replacers = if !stack.config.skip_secret_interp {\n    let VariablesAndSecrets { variables, secrets } =\n      get_variables_and_secrets().await?;\n\n    let mut interpolator =\n      Interpolator::new(Some(&variables), &secrets);\n\n    interpolator.interpolate_stack(&mut stack)?;\n    if let Some(repo) = repo.as_mut()\n      && !repo.config.skip_secret_interp\n    {\n      interpolator.interpolate_repo(repo)?;\n    }\n    if let Some(update) = update {\n      interpolator.push_logs(&mut update.logs);\n    }\n    interpolator.secret_replacers\n  } else {\n    Default::default()\n  };\n\n  let res = periphery_client(server)?\n    .request(ComposePull {\n      stack,\n      services,\n      repo,\n      git_token,\n      registry_token,\n      replacers: secret_replacers.into_iter().collect(),\n    })\n    .await?;\n\n  // Ensure cached stack state up to date by updating server cache\n  update_cache_for_server(server, true).await;\n\n  Ok(res)\n}\n\nimpl Resolve<ExecuteArgs> for PullStack {\n  #[instrument(name = \"PullStack\", skip(user, update), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (stack, server) = get_stack_and_server(\n      &self.stack,\n      user,\n      PermissionLevel::Execute.into(),\n      true,\n    )\n    .await?;\n\n    let repo = if !stack.config.files_on_host\n      && !stack.config.linked_repo.is_empty()\n    {\n      crate::resource::get::<Repo>(&stack.config.linked_repo)\n        .await?\n        .into()\n    } else {\n      None\n    };\n\n    // get the action state for the stack (or insert default).\n    let action_state =\n      action_states().stack.get_or_insert_default(&stack.id).await;\n\n    // Will check to ensure stack not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.pulling = true)?;\n\n    let mut update = update.clone();\n    update_update(update.clone()).await?;\n\n    let res = pull_stack_inner(\n      stack,\n      self.services,\n      &server,\n      repo,\n      Some(&mut update),\n    )\n    .await?;\n\n    update.logs.extend(res.logs);\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for StartStack {\n  #[instrument(name = \"StartStack\", skip(user, update), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    execute_compose::<StartStack>(\n      &self.stack,\n      self.services,\n      user,\n      |state| state.starting = true,\n      update.clone(),\n      (),\n    )\n    .await\n    .map_err(Into::into)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for RestartStack {\n  #[instrument(name = \"RestartStack\", skip(user, update), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    execute_compose::<RestartStack>(\n      &self.stack,\n      self.services,\n      user,\n      |state| {\n        state.restarting = true;\n      },\n      update.clone(),\n      (),\n    )\n    .await\n    .map_err(Into::into)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for PauseStack {\n  #[instrument(name = \"PauseStack\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    execute_compose::<PauseStack>(\n      &self.stack,\n      self.services,\n      user,\n      |state| state.pausing = true,\n      update.clone(),\n      (),\n    )\n    .await\n    .map_err(Into::into)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for UnpauseStack {\n  #[instrument(name = \"UnpauseStack\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    execute_compose::<UnpauseStack>(\n      &self.stack,\n      self.services,\n      user,\n      |state| state.unpausing = true,\n      update.clone(),\n      (),\n    )\n    .await\n    .map_err(Into::into)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for StopStack {\n  #[instrument(name = \"StopStack\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    execute_compose::<StopStack>(\n      &self.stack,\n      self.services,\n      user,\n      |state| state.stopping = true,\n      update.clone(),\n      self.stop_time,\n    )\n    .await\n    .map_err(Into::into)\n  }\n}\n\nimpl super::BatchExecute for BatchDestroyStack {\n  type Resource = Stack;\n  fn single_request(stack: String) -> ExecuteRequest {\n    ExecuteRequest::DestroyStack(DestroyStack {\n      stack,\n      services: Vec::new(),\n      remove_orphans: false,\n      stop_time: None,\n    })\n  }\n}\n\nimpl Resolve<ExecuteArgs> for BatchDestroyStack {\n  #[instrument(name = \"BatchDestroyStack\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, .. }: &ExecuteArgs,\n  ) -> serror::Result<BatchExecutionResponse> {\n    super::batch_execute::<BatchDestroyStack>(&self.pattern, user)\n      .await\n      .map_err(Into::into)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for DestroyStack {\n  #[instrument(name = \"DestroyStack\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    execute_compose::<DestroyStack>(\n      &self.stack,\n      self.services,\n      user,\n      |state| state.destroying = true,\n      update.clone(),\n      (self.stop_time, self.remove_orphans),\n    )\n    .await\n    .map_err(Into::into)\n  }\n}\n\nimpl Resolve<ExecuteArgs> for RunStackService {\n  #[instrument(name = \"RunStackService\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let (mut stack, server) = get_stack_and_server(\n      &self.stack,\n      user,\n      PermissionLevel::Execute.into(),\n      true,\n    )\n    .await?;\n\n    let mut repo = if !stack.config.files_on_host\n      && !stack.config.linked_repo.is_empty()\n    {\n      crate::resource::get::<Repo>(&stack.config.linked_repo)\n        .await?\n        .into()\n    } else {\n      None\n    };\n\n    let action_state =\n      action_states().stack.get_or_insert_default(&stack.id).await;\n\n    let _action_guard =\n      action_state.update(|state| state.deploying = true)?;\n\n    let mut update = update.clone();\n    update_update(update.clone()).await?;\n\n    let git_token =\n      stack_git_token(&mut stack, repo.as_mut()).await?;\n\n    let registry_token = crate::helpers::registry_token(\n      &stack.config.registry_provider,\n      &stack.config.registry_account,\n    ).await.with_context(\n      || format!(\"Failed to get registry token in call to db. Stopping run. | {} | {}\", stack.config.registry_provider, stack.config.registry_account),\n    )?;\n\n    let secret_replacers = if !stack.config.skip_secret_interp {\n      let VariablesAndSecrets { variables, secrets } =\n        get_variables_and_secrets().await?;\n\n      let mut interpolator =\n        Interpolator::new(Some(&variables), &secrets);\n\n      interpolator.interpolate_stack(&mut stack)?;\n      if let Some(repo) = repo.as_mut()\n        && !repo.config.skip_secret_interp\n      {\n        interpolator.interpolate_repo(repo)?;\n      }\n      interpolator.push_logs(&mut update.logs);\n\n      interpolator.secret_replacers\n    } else {\n      Default::default()\n    };\n\n    let log = periphery_client(&server)?\n      .request(ComposeRun {\n        stack,\n        repo,\n        git_token,\n        registry_token,\n        replacers: secret_replacers.into_iter().collect(),\n        service: self.service,\n        command: self.command,\n        no_tty: self.no_tty,\n        no_deps: self.no_deps,\n        detach: self.detach,\n        service_ports: self.service_ports,\n        env: self.env,\n        workdir: self.workdir,\n        user: self.user,\n        entrypoint: self.entrypoint,\n        pull: self.pull,\n      })\n      .await?;\n\n    update.logs.push(log);\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/execute/sync.rs",
    "content": "use std::{collections::HashMap, str::FromStr};\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::update_one_by_id,\n  mongodb::bson::{doc, oid::ObjectId},\n};\nuse formatting::{Color, colored, format_serror};\nuse komodo_client::{\n  api::{execute::RunSync, write::RefreshResourceSyncPending},\n  entities::{\n    self, ResourceTargetVariant,\n    action::Action,\n    alerter::Alerter,\n    build::Build,\n    builder::Builder,\n    deployment::Deployment,\n    komodo_timestamp,\n    permission::PermissionLevel,\n    procedure::Procedure,\n    repo::Repo,\n    server::Server,\n    stack::Stack,\n    sync::ResourceSync,\n    update::{Log, Update},\n    user::sync_user,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  api::write::WriteArgs,\n  helpers::{\n    all_resources::AllResourcesById, query::get_id_to_tags,\n    update::update_update,\n  },\n  permission::get_check_permissions,\n  state::{action_states, db_client},\n  sync::{\n    ResourceSyncTrait,\n    deploy::{\n      SyncDeployParams, build_deploy_cache, deploy_from_cache,\n    },\n    execute::{ExecuteResourceSync, get_updates_for_execution},\n    remote::RemoteResources,\n  },\n};\n\nuse super::ExecuteArgs;\n\nimpl Resolve<ExecuteArgs> for RunSync {\n  #[instrument(name = \"RunSync\", skip(user, update), fields(user_id = user.id, update_id = update.id))]\n  async fn resolve(\n    self,\n    ExecuteArgs { user, update }: &ExecuteArgs,\n  ) -> serror::Result<Update> {\n    let RunSync {\n      sync,\n      resource_type: match_resource_type,\n      resources: match_resources,\n    } = self;\n    let sync = get_check_permissions::<entities::sync::ResourceSync>(\n      &sync,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    let repo = if !sync.config.files_on_host\n      && !sync.config.linked_repo.is_empty()\n    {\n      crate::resource::get::<Repo>(&sync.config.linked_repo)\n        .await?\n        .into()\n    } else {\n      None\n    };\n\n    // get the action state for the sync (or insert default).\n    let action_state =\n      action_states().sync.get_or_insert_default(&sync.id).await;\n\n    // This will set action state back to default when dropped.\n    // Will also check to ensure sync not already busy before updating.\n    let _action_guard =\n      action_state.update(|state| state.syncing = true)?;\n\n    let mut update = update.clone();\n\n    // Send update here for FE to recheck action state\n    update_update(update.clone()).await?;\n\n    let RemoteResources {\n      resources,\n      logs,\n      hash,\n      message,\n      file_errors,\n      ..\n    } =\n      crate::sync::remote::get_remote_resources(&sync, repo.as_ref())\n        .await\n        .context(\"failed to get remote resources\")?;\n\n    update.logs.extend(logs);\n    update_update(update.clone()).await?;\n\n    if !file_errors.is_empty() {\n      return Err(\n        anyhow!(\"Found file errors. Cannot execute sync.\").into(),\n      );\n    }\n\n    let resources = resources?;\n\n    let id_to_tags = get_id_to_tags(None).await?;\n    let all_resources = AllResourcesById::load().await?;\n    // Convert all match_resources to names\n    let match_resources = match_resources.map(|resources| {\n      resources\n        .into_iter()\n        .filter_map(|name_or_id| {\n          let Some(resource_type) = match_resource_type else {\n            return Some(name_or_id);\n          };\n          match ObjectId::from_str(&name_or_id) {\n            Ok(_) => match resource_type {\n              ResourceTargetVariant::Alerter => all_resources\n                .alerters\n                .get(&name_or_id)\n                .map(|a| a.name.clone()),\n              ResourceTargetVariant::Build => all_resources\n                .builds\n                .get(&name_or_id)\n                .map(|b| b.name.clone()),\n              ResourceTargetVariant::Builder => all_resources\n                .builders\n                .get(&name_or_id)\n                .map(|b| b.name.clone()),\n              ResourceTargetVariant::Deployment => all_resources\n                .deployments\n                .get(&name_or_id)\n                .map(|d| d.name.clone()),\n              ResourceTargetVariant::Procedure => all_resources\n                .procedures\n                .get(&name_or_id)\n                .map(|p| p.name.clone()),\n              ResourceTargetVariant::Action => all_resources\n                .actions\n                .get(&name_or_id)\n                .map(|p| p.name.clone()),\n              ResourceTargetVariant::Repo => all_resources\n                .repos\n                .get(&name_or_id)\n                .map(|r| r.name.clone()),\n              ResourceTargetVariant::Server => all_resources\n                .servers\n                .get(&name_or_id)\n                .map(|s| s.name.clone()),\n              ResourceTargetVariant::Stack => all_resources\n                .stacks\n                .get(&name_or_id)\n                .map(|s| s.name.clone()),\n              ResourceTargetVariant::ResourceSync => all_resources\n                .syncs\n                .get(&name_or_id)\n                .map(|s| s.name.clone()),\n              ResourceTargetVariant::System => None,\n            },\n            Err(_) => Some(name_or_id),\n          }\n        })\n        .collect::<Vec<_>>()\n    });\n\n    let deployments_by_name = all_resources\n      .deployments\n      .values()\n      .filter(|deployment| {\n        Deployment::include_resource(\n          &deployment.name,\n          &deployment.config,\n          match_resource_type,\n          match_resources.as_deref(),\n          &deployment.tags,\n          &id_to_tags,\n          &sync.config.match_tags,\n        )\n      })\n      .map(|deployment| (deployment.name.clone(), deployment.clone()))\n      .collect::<HashMap<_, _>>();\n    let stacks_by_name = all_resources\n      .stacks\n      .values()\n      .filter(|stack| {\n        Stack::include_resource(\n          &stack.name,\n          &stack.config,\n          match_resource_type,\n          match_resources.as_deref(),\n          &stack.tags,\n          &id_to_tags,\n          &sync.config.match_tags,\n        )\n      })\n      .map(|stack| (stack.name.clone(), stack.clone()))\n      .collect::<HashMap<_, _>>();\n\n    let deploy_cache = build_deploy_cache(SyncDeployParams {\n      deployments: &resources.deployments,\n      deployment_map: &deployments_by_name,\n      stacks: &resources.stacks,\n      stack_map: &stacks_by_name,\n    })\n    .await?;\n\n    let delete = sync.config.managed || sync.config.delete;\n\n    let server_deltas = if sync.config.include_resources {\n      get_updates_for_execution::<Server>(\n        resources.servers,\n        delete,\n        match_resource_type,\n        match_resources.as_deref(),\n        &id_to_tags,\n        &sync.config.match_tags,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n    let stack_deltas = if sync.config.include_resources {\n      get_updates_for_execution::<Stack>(\n        resources.stacks,\n        delete,\n        match_resource_type,\n        match_resources.as_deref(),\n        &id_to_tags,\n        &sync.config.match_tags,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n    let deployment_deltas = if sync.config.include_resources {\n      get_updates_for_execution::<Deployment>(\n        resources.deployments,\n        delete,\n        match_resource_type,\n        match_resources.as_deref(),\n        &id_to_tags,\n        &sync.config.match_tags,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n    let build_deltas = if sync.config.include_resources {\n      get_updates_for_execution::<Build>(\n        resources.builds,\n        delete,\n        match_resource_type,\n        match_resources.as_deref(),\n        &id_to_tags,\n        &sync.config.match_tags,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n    let repo_deltas = if sync.config.include_resources {\n      get_updates_for_execution::<Repo>(\n        resources.repos,\n        delete,\n        match_resource_type,\n        match_resources.as_deref(),\n        &id_to_tags,\n        &sync.config.match_tags,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n    let procedure_deltas = if sync.config.include_resources {\n      get_updates_for_execution::<Procedure>(\n        resources.procedures,\n        delete,\n        match_resource_type,\n        match_resources.as_deref(),\n        &id_to_tags,\n        &sync.config.match_tags,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n    let action_deltas = if sync.config.include_resources {\n      get_updates_for_execution::<Action>(\n        resources.actions,\n        delete,\n        match_resource_type,\n        match_resources.as_deref(),\n        &id_to_tags,\n        &sync.config.match_tags,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n    let builder_deltas = if sync.config.include_resources {\n      get_updates_for_execution::<Builder>(\n        resources.builders,\n        delete,\n        match_resource_type,\n        match_resources.as_deref(),\n        &id_to_tags,\n        &sync.config.match_tags,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n    let alerter_deltas = if sync.config.include_resources {\n      get_updates_for_execution::<Alerter>(\n        resources.alerters,\n        delete,\n        match_resource_type,\n        match_resources.as_deref(),\n        &id_to_tags,\n        &sync.config.match_tags,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n    let resource_sync_deltas = if sync.config.include_resources {\n      get_updates_for_execution::<entities::sync::ResourceSync>(\n        resources.resource_syncs,\n        delete,\n        match_resource_type,\n        match_resources.as_deref(),\n        &id_to_tags,\n        &sync.config.match_tags,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n\n    let (\n      variables_to_create,\n      variables_to_update,\n      variables_to_delete,\n    ) = if match_resource_type.is_none()\n      && match_resources.is_none()\n      && sync.config.include_variables\n    {\n      crate::sync::variables::get_updates_for_execution(\n        resources.variables,\n        delete,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n    let (\n      user_groups_to_create,\n      user_groups_to_update,\n      user_groups_to_delete,\n    ) = if match_resource_type.is_none()\n      && match_resources.is_none()\n      && sync.config.include_user_groups\n    {\n      crate::sync::user_groups::get_updates_for_execution(\n        resources.user_groups,\n        delete,\n      )\n      .await?\n    } else {\n      Default::default()\n    };\n\n    if deploy_cache.is_empty()\n      && resource_sync_deltas.no_changes()\n      && server_deltas.no_changes()\n      && deployment_deltas.no_changes()\n      && stack_deltas.no_changes()\n      && build_deltas.no_changes()\n      && builder_deltas.no_changes()\n      && alerter_deltas.no_changes()\n      && repo_deltas.no_changes()\n      && procedure_deltas.no_changes()\n      && action_deltas.no_changes()\n      && user_groups_to_create.is_empty()\n      && user_groups_to_update.is_empty()\n      && user_groups_to_delete.is_empty()\n      && variables_to_create.is_empty()\n      && variables_to_update.is_empty()\n      && variables_to_delete.is_empty()\n    {\n      update.push_simple_log(\n        \"No Changes\",\n        format!(\n          \"{}. exiting.\",\n          colored(\"nothing to do\", Color::Green)\n        ),\n      );\n      update.finalize();\n      update_update(update.clone()).await?;\n      return Ok(update);\n    }\n\n    // =================\n\n    // No deps\n    maybe_extend(\n      &mut update.logs,\n      crate::sync::variables::run_updates(\n        variables_to_create,\n        variables_to_update,\n        variables_to_delete,\n      )\n      .await,\n    );\n    maybe_extend(\n      &mut update.logs,\n      crate::sync::user_groups::run_updates(\n        user_groups_to_create,\n        user_groups_to_update,\n        user_groups_to_delete,\n      )\n      .await,\n    );\n    maybe_extend(\n      &mut update.logs,\n      ResourceSync::execute_sync_updates(resource_sync_deltas).await,\n    );\n    maybe_extend(\n      &mut update.logs,\n      Server::execute_sync_updates(server_deltas).await,\n    );\n    maybe_extend(\n      &mut update.logs,\n      Alerter::execute_sync_updates(alerter_deltas).await,\n    );\n    maybe_extend(\n      &mut update.logs,\n      Action::execute_sync_updates(action_deltas).await,\n    );\n\n    // Dependent on server\n    maybe_extend(\n      &mut update.logs,\n      Builder::execute_sync_updates(builder_deltas).await,\n    );\n    maybe_extend(\n      &mut update.logs,\n      Repo::execute_sync_updates(repo_deltas).await,\n    );\n\n    // Dependant on builder\n    maybe_extend(\n      &mut update.logs,\n      Build::execute_sync_updates(build_deltas).await,\n    );\n\n    // Dependant on server / build\n    maybe_extend(\n      &mut update.logs,\n      Deployment::execute_sync_updates(deployment_deltas).await,\n    );\n    // stack only depends on server, but maybe will depend on build later.\n    maybe_extend(\n      &mut update.logs,\n      Stack::execute_sync_updates(stack_deltas).await,\n    );\n\n    // Dependant on everything\n    maybe_extend(\n      &mut update.logs,\n      Procedure::execute_sync_updates(procedure_deltas).await,\n    );\n\n    // Execute the deploy cache\n    deploy_from_cache(deploy_cache, &mut update.logs).await;\n\n    let db = db_client();\n\n    if let Err(e) = update_one_by_id(\n      &db.resource_syncs,\n      &sync.id,\n      doc! {\n        \"$set\": {\n          \"info.last_sync_ts\": komodo_timestamp(),\n          \"info.last_sync_hash\": hash,\n          \"info.last_sync_message\": message,\n        }\n      },\n      None,\n    )\n    .await\n    {\n      warn!(\n        \"failed to update resource sync {} info after sync | {e:#}\",\n        sync.name\n      )\n    }\n\n    if let Err(e) = (RefreshResourceSyncPending { sync: sync.id })\n      .resolve(&WriteArgs {\n        user: sync_user().to_owned(),\n      })\n      .await\n    {\n      warn!(\n        \"failed to refresh sync {} after run | {:#}\",\n        sync.name, e.error\n      );\n      update.push_error_log(\n        \"refresh sync\",\n        format_serror(\n          &e.error\n            .context(\"failed to refresh sync pending after run\")\n            .into(),\n        ),\n      );\n    }\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nfn maybe_extend(logs: &mut Vec<Log>, log: Option<Log>) {\n  if let Some(log) = log {\n    logs.push(log);\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/mod.rs",
    "content": "pub mod auth;\npub mod execute;\npub mod read;\npub mod terminal;\npub mod user;\npub mod write;\n\n#[derive(serde::Deserialize)]\nstruct Variant {\n  variant: String,\n}\n"
  },
  {
    "path": "bin/core/src/api/read/action.rs",
    "content": "use anyhow::Context;\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    action::{\n      Action, ActionActionState, ActionListItem, ActionState,\n    },\n    permission::PermissionLevel,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::query::get_all_tags,\n  permission::get_check_permissions,\n  resource,\n  state::{action_state_cache, action_states},\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetAction {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Action> {\n    Ok(\n      get_check_permissions::<Action>(\n        &self.action,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListActions {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<ActionListItem>> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_for_user::<Action>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListFullActions {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListFullActionsResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_full_for_user::<Action>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for GetActionActionState {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ActionActionState> {\n    let action = get_check_permissions::<Action>(\n      &self.action,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let action_state = action_states()\n      .action\n      .get(&action.id)\n      .await\n      .unwrap_or_default()\n      .get()?;\n    Ok(action_state)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetActionsSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetActionsSummaryResponse> {\n    let actions = resource::list_full_for_user::<Action>(\n      Default::default(),\n      user,\n      PermissionLevel::Read.into(),\n      &[],\n    )\n    .await\n    .context(\"failed to get actions from db\")?;\n\n    let mut res = GetActionsSummaryResponse::default();\n\n    let cache = action_state_cache();\n    let action_states = action_states();\n\n    for action in actions {\n      res.total += 1;\n\n      match (\n        cache.get(&action.id).await.unwrap_or_default(),\n        action_states\n          .action\n          .get(&action.id)\n          .await\n          .unwrap_or_default()\n          .get()?,\n      ) {\n        (_, action_states) if action_states.running > 0 => {\n          res.running += action_states.running;\n        }\n        (ActionState::Ok, _) => res.ok += 1,\n        (ActionState::Failed, _) => res.failed += 1,\n        (ActionState::Unknown, _) => res.unknown += 1,\n        // will never come off the cache in the running state, since that comes from action states\n        (ActionState::Running, _) => unreachable!(),\n      }\n    }\n\n    Ok(res)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/alert.rs",
    "content": "use anyhow::Context;\nuse database::mungos::{\n  by_id::find_one_by_id,\n  find::find_collect,\n  mongodb::{bson::doc, options::FindOptions},\n};\nuse komodo_client::{\n  api::read::{\n    GetAlert, GetAlertResponse, ListAlerts, ListAlertsResponse,\n  },\n  entities::{\n    deployment::Deployment, server::Server, stack::Stack,\n    sync::ResourceSync,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  config::core_config, permission::get_resource_ids_for_user,\n  state::db_client,\n};\n\nuse super::ReadArgs;\n\nconst NUM_ALERTS_PER_PAGE: u64 = 100;\n\nimpl Resolve<ReadArgs> for ListAlerts {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListAlertsResponse> {\n    let mut query = self.query.unwrap_or_default();\n    if !user.admin && !core_config().transparent_mode {\n      let server_ids =\n        get_resource_ids_for_user::<Server>(user).await?;\n      let stack_ids =\n        get_resource_ids_for_user::<Stack>(user).await?;\n      let deployment_ids =\n        get_resource_ids_for_user::<Deployment>(user).await?;\n      let sync_ids =\n        get_resource_ids_for_user::<ResourceSync>(user).await?;\n      query.extend(doc! {\n        \"$or\": [\n          { \"target.type\": \"Server\", \"target.id\": { \"$in\": &server_ids } },\n          { \"target.type\": \"Stack\", \"target.id\": { \"$in\": &stack_ids } },\n          { \"target.type\": \"Deployment\", \"target.id\": { \"$in\": &deployment_ids } },\n          { \"target.type\": \"ResourceSync\", \"target.id\": { \"$in\": &sync_ids } },\n        ]\n      });\n    }\n\n    let alerts = find_collect(\n      &db_client().alerts,\n      query,\n      FindOptions::builder()\n        .sort(doc! { \"ts\": -1 })\n        .limit(NUM_ALERTS_PER_PAGE as i64)\n        .skip(self.page * NUM_ALERTS_PER_PAGE)\n        .build(),\n    )\n    .await\n    .context(\"failed to get alerts from db\")?;\n\n    let next_page = if alerts.len() < NUM_ALERTS_PER_PAGE as usize {\n      None\n    } else {\n      Some((self.page + 1) as i64)\n    };\n\n    let res = ListAlertsResponse { next_page, alerts };\n\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetAlert {\n  async fn resolve(\n    self,\n    _: &ReadArgs,\n  ) -> serror::Result<GetAlertResponse> {\n    Ok(\n      find_one_by_id(&db_client().alerts, &self.id)\n        .await\n        .context(\"failed to query db for alert\")?\n        .context(\"no alert found with given id\")?,\n    )\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/alerter.rs",
    "content": "use anyhow::Context;\nuse database::mongo_indexed::Document;\nuse database::mungos::mongodb::bson::doc;\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    alerter::{Alerter, AlerterListItem},\n    permission::PermissionLevel,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::query::get_all_tags, permission::get_check_permissions,\n  resource, state::db_client,\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetAlerter {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Alerter> {\n    Ok(\n      get_check_permissions::<Alerter>(\n        &self.alerter,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListAlerters {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<AlerterListItem>> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_for_user::<Alerter>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListFullAlerters {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListFullAlertersResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_full_for_user::<Alerter>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for GetAlertersSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetAlertersSummaryResponse> {\n    let query = match resource::get_resource_object_ids_for_user::<\n      Alerter,\n    >(user)\n    .await?\n    {\n      Some(ids) => doc! {\n        \"_id\": { \"$in\": ids }\n      },\n      None => Document::new(),\n    };\n    let total = db_client()\n      .alerters\n      .count_documents(query)\n      .await\n      .context(\"failed to count all alerter documents\")?;\n    let res = GetAlertersSummaryResponse {\n      total: total as u32,\n    };\n    Ok(res)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/build.rs",
    "content": "use std::collections::{HashMap, HashSet};\n\nuse anyhow::Context;\nuse async_timing_util::unix_timestamp_ms;\nuse database::mungos::{\n  find::find_collect,\n  mongodb::{bson::doc, options::FindOptions},\n};\nuse futures::TryStreamExt;\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    Operation,\n    build::{Build, BuildActionState, BuildListItem, BuildState},\n    config::core::CoreConfig,\n    permission::PermissionLevel,\n    update::UpdateStatus,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  config::core_config,\n  helpers::query::get_all_tags,\n  permission::get_check_permissions,\n  resource,\n  state::{\n    action_states, build_state_cache, db_client, github_client,\n  },\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetBuild {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Build> {\n    Ok(\n      get_check_permissions::<Build>(\n        &self.build,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListBuilds {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<BuildListItem>> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_for_user::<Build>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListFullBuilds {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListFullBuildsResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_full_for_user::<Build>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for GetBuildActionState {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<BuildActionState> {\n    let build = get_check_permissions::<Build>(\n      &self.build,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let action_state = action_states()\n      .build\n      .get(&build.id)\n      .await\n      .unwrap_or_default()\n      .get()?;\n    Ok(action_state)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetBuildsSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetBuildsSummaryResponse> {\n    let builds = resource::list_full_for_user::<Build>(\n      Default::default(),\n      user,\n      PermissionLevel::Read.into(),\n      &[],\n    )\n    .await\n    .context(\"failed to get all builds\")?;\n\n    let mut res = GetBuildsSummaryResponse::default();\n\n    let cache = build_state_cache();\n    let action_states = action_states();\n\n    for build in builds {\n      res.total += 1;\n\n      match (\n        cache.get(&build.id).await.unwrap_or_default(),\n        action_states\n          .build\n          .get(&build.id)\n          .await\n          .unwrap_or_default()\n          .get()?,\n      ) {\n        (_, action_states) if action_states.building => {\n          res.building += 1;\n        }\n        (BuildState::Ok, _) => res.ok += 1,\n        (BuildState::Failed, _) => res.failed += 1,\n        (BuildState::Unknown, _) => res.unknown += 1,\n        // will never come off the cache in the building state, since that comes from action states\n        (BuildState::Building, _) => unreachable!(),\n      }\n    }\n\n    Ok(res)\n  }\n}\n\nconst ONE_DAY_MS: i64 = 86400000;\n\nimpl Resolve<ReadArgs> for GetBuildMonthlyStats {\n  async fn resolve(\n    self,\n    _: &ReadArgs,\n  ) -> serror::Result<GetBuildMonthlyStatsResponse> {\n    let curr_ts = unix_timestamp_ms() as i64;\n    let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS;\n\n    let close_ts = next_day - self.page as i64 * 30 * ONE_DAY_MS;\n    let open_ts = close_ts - 30 * ONE_DAY_MS;\n\n    let mut build_updates = db_client()\n      .updates\n      .find(doc! {\n        \"start_ts\": {\n          \"$gte\": open_ts,\n          \"$lt\": close_ts\n        },\n        \"operation\": Operation::RunBuild.to_string(),\n      })\n      .await\n      .context(\"failed to get updates cursor\")?;\n\n    let mut days = HashMap::<i64, BuildStatsDay>::with_capacity(32);\n\n    let mut curr = open_ts;\n\n    while curr < close_ts {\n      let stats = BuildStatsDay {\n        ts: curr as f64,\n        ..Default::default()\n      };\n      days.insert(curr, stats);\n      curr += ONE_DAY_MS;\n    }\n\n    while let Some(update) = build_updates.try_next().await? {\n      if let Some(end_ts) = update.end_ts {\n        let day = update.start_ts - update.start_ts % ONE_DAY_MS;\n        let entry = days.entry(day).or_default();\n        entry.count += 1.0;\n        entry.time += ms_to_hour(end_ts - update.start_ts);\n      }\n    }\n\n    Ok(GetBuildMonthlyStatsResponse::new(\n      days.into_values().collect(),\n    ))\n  }\n}\n\nconst MS_TO_HOUR_DIVISOR: f64 = 1000.0 * 60.0 * 60.0;\nfn ms_to_hour(duration: i64) -> f64 {\n  duration as f64 / MS_TO_HOUR_DIVISOR\n}\n\nimpl Resolve<ReadArgs> for ListBuildVersions {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<BuildVersionResponseItem>> {\n    let ListBuildVersions {\n      build,\n      major,\n      minor,\n      patch,\n      limit,\n    } = self;\n    let build = get_check_permissions::<Build>(\n      &build,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n\n    let mut filter = doc! {\n      \"target\": {\n        \"type\": \"Build\",\n        \"id\": build.id\n      },\n      \"operation\": Operation::RunBuild.to_string(),\n      \"status\": UpdateStatus::Complete.to_string(),\n      \"success\": true\n    };\n    if let Some(major) = major {\n      filter.insert(\"version.major\", major);\n    }\n    if let Some(minor) = minor {\n      filter.insert(\"version.minor\", minor);\n    }\n    if let Some(patch) = patch {\n      filter.insert(\"version.patch\", patch);\n    }\n\n    let versions = find_collect(\n      &db_client().updates,\n      filter,\n      FindOptions::builder()\n        .sort(doc! { \"_id\": -1 })\n        .limit(limit)\n        .build(),\n    )\n    .await\n    .context(\"failed to pull versions from mongo\")?\n    .into_iter()\n    .map(|u| (u.version, u.start_ts))\n    .filter(|(v, _)| !v.is_none())\n    .map(|(version, ts)| BuildVersionResponseItem { version, ts })\n    .collect();\n    Ok(versions)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListCommonBuildExtraArgs {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListCommonBuildExtraArgsResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    let builds = resource::list_full_for_user::<Build>(\n      self.query,\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await\n    .context(\"failed to get resources matching query\")?;\n\n    // first collect with guaranteed uniqueness\n    let mut res = HashSet::<String>::new();\n\n    for build in builds {\n      for extra_arg in build.config.extra_args {\n        res.insert(extra_arg);\n      }\n    }\n\n    let mut res = res.into_iter().collect::<Vec<_>>();\n    res.sort();\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetBuildWebhookEnabled {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetBuildWebhookEnabledResponse> {\n    let Some(github) = github_client() else {\n      return Ok(GetBuildWebhookEnabledResponse {\n        managed: false,\n        enabled: false,\n      });\n    };\n\n    let build = get_check_permissions::<Build>(\n      &self.build,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n\n    if build.config.git_provider != \"github.com\"\n      || build.config.repo.is_empty()\n    {\n      return Ok(GetBuildWebhookEnabledResponse {\n        managed: false,\n        enabled: false,\n      });\n    }\n\n    let mut split = build.config.repo.split('/');\n    let owner = split.next().context(\"Build repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Ok(GetBuildWebhookEnabledResponse {\n        managed: false,\n        enabled: false,\n      });\n    };\n\n    let repo =\n      split.next().context(\"Build repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      ..\n    } = core_config();\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let url = format!(\"{host}/listener/github/build/{}\", build.id);\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == url {\n        return Ok(GetBuildWebhookEnabledResponse {\n          managed: true,\n          enabled: true,\n        });\n      }\n    }\n\n    Ok(GetBuildWebhookEnabledResponse {\n      managed: true,\n      enabled: false,\n    })\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/builder.rs",
    "content": "use anyhow::Context;\nuse database::mongo_indexed::Document;\nuse database::mungos::mongodb::bson::doc;\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    builder::{Builder, BuilderListItem},\n    permission::PermissionLevel,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::query::get_all_tags, permission::get_check_permissions,\n  resource, state::db_client,\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetBuilder {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Builder> {\n    Ok(\n      get_check_permissions::<Builder>(\n        &self.builder,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListBuilders {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<BuilderListItem>> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_for_user::<Builder>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListFullBuilders {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListFullBuildersResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_full_for_user::<Builder>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for GetBuildersSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetBuildersSummaryResponse> {\n    let query = match resource::get_resource_object_ids_for_user::<\n      Builder,\n    >(user)\n    .await?\n    {\n      Some(ids) => doc! {\n        \"_id\": { \"$in\": ids }\n      },\n      None => Document::new(),\n    };\n    let total = db_client()\n      .builders\n      .count_documents(query)\n      .await\n      .context(\"failed to count all builder documents\")?;\n    let res = GetBuildersSummaryResponse {\n      total: total as u32,\n    };\n    Ok(res)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/deployment.rs",
    "content": "use std::{cmp, collections::HashSet};\n\nuse anyhow::{Context, anyhow};\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    deployment::{\n      Deployment, DeploymentActionState, DeploymentConfig,\n      DeploymentListItem, DeploymentState,\n    },\n    docker::container::{Container, ContainerStats},\n    permission::PermissionLevel,\n    server::{Server, ServerState},\n    update::Log,\n  },\n};\nuse periphery_client::api::{self, container::InspectContainer};\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::{periphery_client, query::get_all_tags},\n  permission::get_check_permissions,\n  resource,\n  state::{\n    action_states, deployment_status_cache, server_status_cache,\n  },\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetDeployment {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Deployment> {\n    Ok(\n      get_check_permissions::<Deployment>(\n        &self.deployment,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListDeployments {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<DeploymentListItem>> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    let only_update_available = self.query.specific.update_available;\n    let deployments = resource::list_for_user::<Deployment>(\n      self.query,\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?;\n    let deployments = if only_update_available {\n      deployments\n        .into_iter()\n        .filter(|deployment| deployment.info.update_available)\n        .collect()\n    } else {\n      deployments\n    };\n    Ok(deployments)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListFullDeployments {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListFullDeploymentsResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_full_for_user::<Deployment>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for GetDeploymentContainer {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetDeploymentContainerResponse> {\n    let deployment = get_check_permissions::<Deployment>(\n      &self.deployment,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let status = deployment_status_cache()\n      .get(&deployment.id)\n      .await\n      .unwrap_or_default();\n    let response = GetDeploymentContainerResponse {\n      state: status.curr.state,\n      container: status.curr.container.clone(),\n    };\n    Ok(response)\n  }\n}\n\nconst MAX_LOG_LENGTH: u64 = 5000;\n\nimpl Resolve<ReadArgs> for GetDeploymentLog {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Log> {\n    let GetDeploymentLog {\n      deployment,\n      tail,\n      timestamps,\n    } = self;\n    let Deployment {\n      name,\n      config: DeploymentConfig { server_id, .. },\n      ..\n    } = get_check_permissions::<Deployment>(\n      &deployment,\n      user,\n      PermissionLevel::Read.logs(),\n    )\n    .await?;\n    if server_id.is_empty() {\n      return Ok(Log::default());\n    }\n    let server = resource::get::<Server>(&server_id).await?;\n    let res = periphery_client(&server)?\n      .request(api::container::GetContainerLog {\n        name,\n        tail: cmp::min(tail, MAX_LOG_LENGTH),\n        timestamps,\n      })\n      .await\n      .context(\"failed at call to periphery\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for SearchDeploymentLog {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Log> {\n    let SearchDeploymentLog {\n      deployment,\n      terms,\n      combinator,\n      invert,\n      timestamps,\n    } = self;\n    let Deployment {\n      name,\n      config: DeploymentConfig { server_id, .. },\n      ..\n    } = get_check_permissions::<Deployment>(\n      &deployment,\n      user,\n      PermissionLevel::Read.logs(),\n    )\n    .await?;\n    if server_id.is_empty() {\n      return Ok(Log::default());\n    }\n    let server = resource::get::<Server>(&server_id).await?;\n    let res = periphery_client(&server)?\n      .request(api::container::GetContainerLogSearch {\n        name,\n        terms,\n        combinator,\n        invert,\n        timestamps,\n      })\n      .await\n      .context(\"failed at call to periphery\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for InspectDeploymentContainer {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Container> {\n    let InspectDeploymentContainer { deployment } = self;\n    let Deployment {\n      name,\n      config: DeploymentConfig { server_id, .. },\n      ..\n    } = get_check_permissions::<Deployment>(\n      &deployment,\n      user,\n      PermissionLevel::Read.inspect(),\n    )\n    .await?;\n    if server_id.is_empty() {\n      return Err(\n        anyhow!(\n          \"Cannot inspect deployment, not attached to any server\"\n        )\n        .into(),\n      );\n    }\n    let server = resource::get::<Server>(&server_id).await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if cache.state != ServerState::Ok {\n      return Err(\n        anyhow!(\n          \"Cannot inspect container: server is {:?}\",\n          cache.state\n        )\n        .into(),\n      );\n    }\n    let res = periphery_client(&server)?\n      .request(InspectContainer { name })\n      .await?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetDeploymentStats {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ContainerStats> {\n    let Deployment {\n      name,\n      config: DeploymentConfig { server_id, .. },\n      ..\n    } = get_check_permissions::<Deployment>(\n      &self.deployment,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    if server_id.is_empty() {\n      return Err(\n        anyhow!(\"deployment has no server attached\").into(),\n      );\n    }\n    let server = resource::get::<Server>(&server_id).await?;\n    let res = periphery_client(&server)?\n      .request(api::container::GetContainerStats { name })\n      .await\n      .context(\"failed to get stats from periphery\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetDeploymentActionState {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<DeploymentActionState> {\n    let deployment = get_check_permissions::<Deployment>(\n      &self.deployment,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let action_state = action_states()\n      .deployment\n      .get(&deployment.id)\n      .await\n      .unwrap_or_default()\n      .get()?;\n    Ok(action_state)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetDeploymentsSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetDeploymentsSummaryResponse> {\n    let deployments = resource::list_full_for_user::<Deployment>(\n      Default::default(),\n      user,\n      PermissionLevel::Read.into(),\n      &[],\n    )\n    .await\n    .context(\"failed to get deployments from db\")?;\n    let mut res = GetDeploymentsSummaryResponse::default();\n    let status_cache = deployment_status_cache();\n    for deployment in deployments {\n      res.total += 1;\n      let status =\n        status_cache.get(&deployment.id).await.unwrap_or_default();\n      match status.curr.state {\n        DeploymentState::Running => {\n          res.running += 1;\n        }\n        DeploymentState::Exited | DeploymentState::Paused => {\n          res.stopped += 1;\n        }\n        DeploymentState::NotDeployed => {\n          res.not_deployed += 1;\n        }\n        DeploymentState::Unknown => {\n          res.unknown += 1;\n        }\n        _ => {\n          res.unhealthy += 1;\n        }\n      }\n    }\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListCommonDeploymentExtraArgsResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    let deployments = resource::list_full_for_user::<Deployment>(\n      self.query,\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await\n    .context(\"failed to get resources matching query\")?;\n\n    // first collect with guaranteed uniqueness\n    let mut res = HashSet::<String>::new();\n\n    for deployment in deployments {\n      for extra_arg in deployment.config.extra_args {\n        res.insert(extra_arg);\n      }\n    }\n\n    let mut res = res.into_iter().collect::<Vec<_>>();\n    res.sort();\n    Ok(res)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/mod.rs",
    "content": "use std::{collections::HashSet, sync::OnceLock, time::Instant};\n\nuse anyhow::{Context, anyhow};\nuse axum::{\n  Extension, Router, extract::Path, middleware, routing::post,\n};\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    ResourceTarget,\n    build::Build,\n    builder::{Builder, BuilderConfig},\n    config::{DockerRegistry, GitProvider},\n    permission::PermissionLevel,\n    repo::Repo,\n    server::Server,\n    sync::ResourceSync,\n    user::User,\n  },\n};\nuse resolver_api::Resolve;\nuse response::Response;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse serror::Json;\nuse typeshare::typeshare;\nuse uuid::Uuid;\n\nuse crate::{\n  auth::auth_request, config::core_config, helpers::periphery_client,\n  resource,\n};\n\nuse super::Variant;\n\nmod action;\nmod alert;\nmod alerter;\nmod build;\nmod builder;\nmod deployment;\nmod permission;\nmod procedure;\nmod provider;\nmod repo;\nmod schedule;\nmod server;\nmod stack;\nmod sync;\nmod tag;\nmod toml;\nmod update;\nmod user;\nmod user_group;\nmod variable;\n\npub struct ReadArgs {\n  pub user: User,\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[args(ReadArgs)]\n#[response(Response)]\n#[error(serror::Error)]\n#[serde(tag = \"type\", content = \"params\")]\nenum ReadRequest {\n  GetVersion(GetVersion),\n  GetCoreInfo(GetCoreInfo),\n  ListSecrets(ListSecrets),\n  ListGitProvidersFromConfig(ListGitProvidersFromConfig),\n  ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig),\n\n  // ==== USER ====\n  GetUsername(GetUsername),\n  GetPermission(GetPermission),\n  FindUser(FindUser),\n  ListUsers(ListUsers),\n  ListApiKeys(ListApiKeys),\n  ListApiKeysForServiceUser(ListApiKeysForServiceUser),\n  ListPermissions(ListPermissions),\n  ListUserTargetPermissions(ListUserTargetPermissions),\n\n  // ==== USER GROUP ====\n  GetUserGroup(GetUserGroup),\n  ListUserGroups(ListUserGroups),\n\n  // ==== PROCEDURE ====\n  GetProceduresSummary(GetProceduresSummary),\n  GetProcedure(GetProcedure),\n  GetProcedureActionState(GetProcedureActionState),\n  ListProcedures(ListProcedures),\n  ListFullProcedures(ListFullProcedures),\n\n  // ==== ACTION ====\n  GetActionsSummary(GetActionsSummary),\n  GetAction(GetAction),\n  GetActionActionState(GetActionActionState),\n  ListActions(ListActions),\n  ListFullActions(ListFullActions),\n\n  // ==== SCHEDULE ====\n  ListSchedules(ListSchedules),\n\n  // ==== SERVER ====\n  GetServersSummary(GetServersSummary),\n  GetServer(GetServer),\n  GetServerState(GetServerState),\n  GetPeripheryVersion(GetPeripheryVersion),\n  GetServerActionState(GetServerActionState),\n  GetHistoricalServerStats(GetHistoricalServerStats),\n  ListServers(ListServers),\n  ListFullServers(ListFullServers),\n  InspectDockerContainer(InspectDockerContainer),\n  GetResourceMatchingContainer(GetResourceMatchingContainer),\n  GetContainerLog(GetContainerLog),\n  SearchContainerLog(SearchContainerLog),\n  InspectDockerNetwork(InspectDockerNetwork),\n  InspectDockerImage(InspectDockerImage),\n  ListDockerImageHistory(ListDockerImageHistory),\n  InspectDockerVolume(InspectDockerVolume),\n  GetDockerContainersSummary(GetDockerContainersSummary),\n  ListAllDockerContainers(ListAllDockerContainers),\n  ListDockerContainers(ListDockerContainers),\n  ListDockerNetworks(ListDockerNetworks),\n  ListDockerImages(ListDockerImages),\n  ListDockerVolumes(ListDockerVolumes),\n  ListComposeProjects(ListComposeProjects),\n  ListTerminals(ListTerminals),\n\n  // ==== SERVER STATS ====\n  GetSystemInformation(GetSystemInformation),\n  GetSystemStats(GetSystemStats),\n  ListSystemProcesses(ListSystemProcesses),\n\n  // ==== STACK ====\n  GetStacksSummary(GetStacksSummary),\n  GetStack(GetStack),\n  GetStackActionState(GetStackActionState),\n  GetStackWebhooksEnabled(GetStackWebhooksEnabled),\n  GetStackLog(GetStackLog),\n  SearchStackLog(SearchStackLog),\n  InspectStackContainer(InspectStackContainer),\n  ListStacks(ListStacks),\n  ListFullStacks(ListFullStacks),\n  ListStackServices(ListStackServices),\n  ListCommonStackExtraArgs(ListCommonStackExtraArgs),\n  ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),\n\n  // ==== DEPLOYMENT ====\n  GetDeploymentsSummary(GetDeploymentsSummary),\n  GetDeployment(GetDeployment),\n  GetDeploymentContainer(GetDeploymentContainer),\n  GetDeploymentActionState(GetDeploymentActionState),\n  GetDeploymentStats(GetDeploymentStats),\n  GetDeploymentLog(GetDeploymentLog),\n  SearchDeploymentLog(SearchDeploymentLog),\n  InspectDeploymentContainer(InspectDeploymentContainer),\n  ListDeployments(ListDeployments),\n  ListFullDeployments(ListFullDeployments),\n  ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs),\n\n  // ==== BUILD ====\n  GetBuildsSummary(GetBuildsSummary),\n  GetBuild(GetBuild),\n  GetBuildActionState(GetBuildActionState),\n  GetBuildMonthlyStats(GetBuildMonthlyStats),\n  ListBuildVersions(ListBuildVersions),\n  GetBuildWebhookEnabled(GetBuildWebhookEnabled),\n  ListBuilds(ListBuilds),\n  ListFullBuilds(ListFullBuilds),\n  ListCommonBuildExtraArgs(ListCommonBuildExtraArgs),\n\n  // ==== REPO ====\n  GetReposSummary(GetReposSummary),\n  GetRepo(GetRepo),\n  GetRepoActionState(GetRepoActionState),\n  GetRepoWebhooksEnabled(GetRepoWebhooksEnabled),\n  ListRepos(ListRepos),\n  ListFullRepos(ListFullRepos),\n\n  // ==== SYNC ====\n  GetResourceSyncsSummary(GetResourceSyncsSummary),\n  GetResourceSync(GetResourceSync),\n  GetResourceSyncActionState(GetResourceSyncActionState),\n  GetSyncWebhooksEnabled(GetSyncWebhooksEnabled),\n  ListResourceSyncs(ListResourceSyncs),\n  ListFullResourceSyncs(ListFullResourceSyncs),\n\n  // ==== BUILDER ====\n  GetBuildersSummary(GetBuildersSummary),\n  GetBuilder(GetBuilder),\n  ListBuilders(ListBuilders),\n  ListFullBuilders(ListFullBuilders),\n\n  // ==== ALERTER ====\n  GetAlertersSummary(GetAlertersSummary),\n  GetAlerter(GetAlerter),\n  ListAlerters(ListAlerters),\n  ListFullAlerters(ListFullAlerters),\n\n  // ==== TOML ====\n  ExportAllResourcesToToml(ExportAllResourcesToToml),\n  ExportResourcesToToml(ExportResourcesToToml),\n\n  // ==== TAG ====\n  GetTag(GetTag),\n  ListTags(ListTags),\n\n  // ==== UPDATE ====\n  GetUpdate(GetUpdate),\n  ListUpdates(ListUpdates),\n\n  // ==== ALERT ====\n  ListAlerts(ListAlerts),\n  GetAlert(GetAlert),\n\n  // ==== VARIABLE ====\n  GetVariable(GetVariable),\n  ListVariables(ListVariables),\n\n  // ==== PROVIDER ====\n  GetGitProviderAccount(GetGitProviderAccount),\n  ListGitProviderAccounts(ListGitProviderAccounts),\n  GetDockerRegistryAccount(GetDockerRegistryAccount),\n  ListDockerRegistryAccounts(ListDockerRegistryAccounts),\n}\n\npub fn router() -> Router {\n  Router::new()\n    .route(\"/\", post(handler))\n    .route(\"/{variant}\", post(variant_handler))\n    .layer(middleware::from_fn(auth_request))\n}\n\nasync fn variant_handler(\n  user: Extension<User>,\n  Path(Variant { variant }): Path<Variant>,\n  Json(params): Json<serde_json::Value>,\n) -> serror::Result<axum::response::Response> {\n  let req: ReadRequest = serde_json::from_value(json!({\n    \"type\": variant,\n    \"params\": params,\n  }))?;\n  handler(user, Json(req)).await\n}\n\n#[instrument(name = \"ReadHandler\", level = \"debug\", skip(user), fields(user_id = user.id))]\nasync fn handler(\n  Extension(user): Extension<User>,\n  Json(request): Json<ReadRequest>,\n) -> serror::Result<axum::response::Response> {\n  let timer = Instant::now();\n  let req_id = Uuid::new_v4();\n  debug!(\"/read request | user: {}\", user.username);\n  let res = request.resolve(&ReadArgs { user }).await;\n  if let Err(e) = &res {\n    debug!(\"/read request {req_id} error: {:#}\", e.error);\n  }\n  let elapsed = timer.elapsed();\n  debug!(\"/read request {req_id} | resolve time: {elapsed:?}\");\n  res.map(|res| res.0)\n}\n\nimpl Resolve<ReadArgs> for GetVersion {\n  async fn resolve(\n    self,\n    _: &ReadArgs,\n  ) -> serror::Result<GetVersionResponse> {\n    Ok(GetVersionResponse {\n      version: env!(\"CARGO_PKG_VERSION\").to_string(),\n    })\n  }\n}\n\nfn core_info() -> &'static GetCoreInfoResponse {\n  static CORE_INFO: OnceLock<GetCoreInfoResponse> = OnceLock::new();\n  CORE_INFO.get_or_init(|| {\n    let config = core_config();\n    GetCoreInfoResponse {\n      title: config.title.clone(),\n      monitoring_interval: config.monitoring_interval,\n      webhook_base_url: if config.webhook_base_url.is_empty() {\n        config.host.clone()\n      } else {\n        config.webhook_base_url.clone()\n      },\n      transparent_mode: config.transparent_mode,\n      ui_write_disabled: config.ui_write_disabled,\n      disable_confirm_dialog: config.disable_confirm_dialog,\n      disable_non_admin_create: config.disable_non_admin_create,\n      disable_websocket_reconnect: config.disable_websocket_reconnect,\n      enable_fancy_toml: config.enable_fancy_toml,\n      github_webhook_owners: config\n        .github_webhook_app\n        .installations\n        .iter()\n        .map(|i| i.namespace.to_string())\n        .collect(),\n      timezone: config.timezone.clone(),\n    }\n  })\n}\n\nimpl Resolve<ReadArgs> for GetCoreInfo {\n  async fn resolve(\n    self,\n    _: &ReadArgs,\n  ) -> serror::Result<GetCoreInfoResponse> {\n    Ok(core_info().clone())\n  }\n}\n\nimpl Resolve<ReadArgs> for ListSecrets {\n  async fn resolve(\n    self,\n    _: &ReadArgs,\n  ) -> serror::Result<ListSecretsResponse> {\n    let mut secrets = core_config()\n      .secrets\n      .keys()\n      .cloned()\n      .collect::<HashSet<_>>();\n\n    if let Some(target) = self.target {\n      let server_id = match target {\n        ResourceTarget::Server(id) => Some(id),\n        ResourceTarget::Builder(id) => {\n          match resource::get::<Builder>(&id).await?.config {\n            BuilderConfig::Url(_) => None,\n            BuilderConfig::Server(config) => Some(config.server_id),\n            BuilderConfig::Aws(config) => {\n              secrets.extend(config.secrets);\n              None\n            }\n          }\n        }\n        _ => {\n          return Err(\n            anyhow!(\"target must be `Server` or `Builder`\").into(),\n          );\n        }\n      };\n      if let Some(id) = server_id {\n        let server = resource::get::<Server>(&id).await?;\n        let more = periphery_client(&server)?\n          .request(periphery_client::api::ListSecrets {})\n          .await\n          .with_context(|| {\n            format!(\n              \"failed to get secrets from server {}\",\n              server.name\n            )\n          })?;\n        secrets.extend(more);\n      }\n    }\n\n    let mut secrets = secrets.into_iter().collect::<Vec<_>>();\n    secrets.sort();\n\n    Ok(secrets)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListGitProvidersFromConfig {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListGitProvidersFromConfigResponse> {\n    let mut providers = core_config().git_providers.clone();\n\n    if let Some(target) = self.target {\n      match target {\n        ResourceTarget::Server(id) => {\n          merge_git_providers_for_server(&mut providers, &id).await?;\n        }\n        ResourceTarget::Builder(id) => {\n          match resource::get::<Builder>(&id).await?.config {\n            BuilderConfig::Url(_) => {}\n            BuilderConfig::Server(config) => {\n              merge_git_providers_for_server(\n                &mut providers,\n                &config.server_id,\n              )\n              .await?;\n            }\n            BuilderConfig::Aws(config) => {\n              merge_git_providers(\n                &mut providers,\n                config.git_providers,\n              );\n            }\n          }\n        }\n        _ => {\n          return Err(\n            anyhow!(\"target must be `Server` or `Builder`\").into(),\n          );\n        }\n      }\n    }\n\n    let (builds, repos, syncs) = tokio::try_join!(\n      resource::list_full_for_user::<Build>(\n        Default::default(),\n        user,\n        PermissionLevel::Read.into(),\n        &[]\n      ),\n      resource::list_full_for_user::<Repo>(\n        Default::default(),\n        user,\n        PermissionLevel::Read.into(),\n        &[]\n      ),\n      resource::list_full_for_user::<ResourceSync>(\n        Default::default(),\n        user,\n        PermissionLevel::Read.into(),\n        &[]\n      ),\n    )?;\n\n    for build in builds {\n      if !providers\n        .iter()\n        .any(|provider| provider.domain == build.config.git_provider)\n      {\n        providers.push(GitProvider {\n          domain: build.config.git_provider,\n          https: build.config.git_https,\n          accounts: Default::default(),\n        });\n      }\n    }\n    for repo in repos {\n      if !providers\n        .iter()\n        .any(|provider| provider.domain == repo.config.git_provider)\n      {\n        providers.push(GitProvider {\n          domain: repo.config.git_provider,\n          https: repo.config.git_https,\n          accounts: Default::default(),\n        });\n      }\n    }\n    for sync in syncs {\n      if !providers\n        .iter()\n        .any(|provider| provider.domain == sync.config.git_provider)\n      {\n        providers.push(GitProvider {\n          domain: sync.config.git_provider,\n          https: sync.config.git_https,\n          accounts: Default::default(),\n        });\n      }\n    }\n\n    providers.sort();\n\n    Ok(providers)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {\n  async fn resolve(\n    self,\n    _: &ReadArgs,\n  ) -> serror::Result<ListDockerRegistriesFromConfigResponse> {\n    let mut registries = core_config().docker_registries.clone();\n\n    if let Some(target) = self.target {\n      match target {\n        ResourceTarget::Server(id) => {\n          merge_docker_registries_for_server(&mut registries, &id)\n            .await?;\n        }\n        ResourceTarget::Builder(id) => {\n          match resource::get::<Builder>(&id).await?.config {\n            BuilderConfig::Url(_) => {}\n            BuilderConfig::Server(config) => {\n              merge_docker_registries_for_server(\n                &mut registries,\n                &config.server_id,\n              )\n              .await?;\n            }\n            BuilderConfig::Aws(config) => {\n              merge_docker_registries(\n                &mut registries,\n                config.docker_registries,\n              );\n            }\n          }\n        }\n        _ => {\n          return Err(\n            anyhow!(\"target must be `Server` or `Builder`\").into(),\n          );\n        }\n      }\n    }\n\n    registries.sort();\n\n    Ok(registries)\n  }\n}\n\nasync fn merge_git_providers_for_server(\n  providers: &mut Vec<GitProvider>,\n  server_id: &str,\n) -> serror::Result<()> {\n  let server = resource::get::<Server>(server_id).await?;\n  let more = periphery_client(&server)?\n    .request(periphery_client::api::ListGitProviders {})\n    .await\n    .with_context(|| {\n      format!(\n        \"failed to get git providers from server {}\",\n        server.name\n      )\n    })?;\n  merge_git_providers(providers, more);\n  Ok(())\n}\n\nfn merge_git_providers(\n  providers: &mut Vec<GitProvider>,\n  more: Vec<GitProvider>,\n) {\n  for incoming_provider in more {\n    if let Some(provider) = providers\n      .iter_mut()\n      .find(|provider| provider.domain == incoming_provider.domain)\n    {\n      for account in incoming_provider.accounts {\n        if !provider.accounts.contains(&account) {\n          provider.accounts.push(account);\n        }\n      }\n    } else {\n      providers.push(incoming_provider);\n    }\n  }\n}\n\nasync fn merge_docker_registries_for_server(\n  registries: &mut Vec<DockerRegistry>,\n  server_id: &str,\n) -> serror::Result<()> {\n  let server = resource::get::<Server>(server_id).await?;\n  let more = periphery_client(&server)?\n    .request(periphery_client::api::ListDockerRegistries {})\n    .await\n    .with_context(|| {\n      format!(\n        \"failed to get docker registries from server {}\",\n        server.name\n      )\n    })?;\n  merge_docker_registries(registries, more);\n  Ok(())\n}\n\nfn merge_docker_registries(\n  registries: &mut Vec<DockerRegistry>,\n  more: Vec<DockerRegistry>,\n) {\n  for incoming_registry in more {\n    if let Some(registry) = registries\n      .iter_mut()\n      .find(|registry| registry.domain == incoming_registry.domain)\n    {\n      for account in incoming_registry.accounts {\n        if !registry.accounts.contains(&account) {\n          registry.accounts.push(account);\n        }\n      }\n    } else {\n      registries.push(incoming_registry);\n    }\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/permission.rs",
    "content": "use anyhow::{Context, anyhow};\nuse database::mungos::{find::find_collect, mongodb::bson::doc};\nuse komodo_client::{\n  api::read::{\n    GetPermission, GetPermissionResponse, ListPermissions,\n    ListPermissionsResponse, ListUserTargetPermissions,\n    ListUserTargetPermissionsResponse,\n  },\n  entities::permission::PermissionLevel,\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::query::get_user_permission_on_target, state::db_client,\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for ListPermissions {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListPermissionsResponse> {\n    let res = find_collect(\n      &db_client().permissions,\n      doc! {\n        \"user_target.type\": \"User\",\n        \"user_target.id\": &user.id\n      },\n      None,\n    )\n    .await\n    .context(\"failed to query db for permissions\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetPermission {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetPermissionResponse> {\n    if user.admin {\n      return Ok(PermissionLevel::Write.all());\n    }\n    Ok(get_user_permission_on_target(user, &self.target).await?)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListUserTargetPermissions {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListUserTargetPermissionsResponse> {\n    if !user.admin {\n      return Err(anyhow!(\"this method is admin only\").into());\n    }\n    let (variant, id) = self.user_target.extract_variant_id();\n    let res = find_collect(\n      &db_client().permissions,\n      doc! {\n        \"user_target.type\": variant.as_ref(),\n        \"user_target.id\": id\n      },\n      None,\n    )\n    .await\n    .context(\"failed to query db for permissions\")?;\n    Ok(res)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/procedure.rs",
    "content": "use anyhow::Context;\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    permission::PermissionLevel,\n    procedure::{Procedure, ProcedureState},\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::query::get_all_tags,\n  permission::get_check_permissions,\n  resource,\n  state::{action_states, procedure_state_cache},\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetProcedure {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetProcedureResponse> {\n    Ok(\n      get_check_permissions::<Procedure>(\n        &self.procedure,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListProcedures {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListProceduresResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_for_user::<Procedure>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListFullProcedures {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListFullProceduresResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_full_for_user::<Procedure>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for GetProceduresSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetProceduresSummaryResponse> {\n    let procedures = resource::list_full_for_user::<Procedure>(\n      Default::default(),\n      user,\n      PermissionLevel::Read.into(),\n      &[],\n    )\n    .await\n    .context(\"failed to get procedures from db\")?;\n\n    let mut res = GetProceduresSummaryResponse::default();\n\n    let cache = procedure_state_cache();\n    let action_states = action_states();\n\n    for procedure in procedures {\n      res.total += 1;\n\n      match (\n        cache.get(&procedure.id).await.unwrap_or_default(),\n        action_states\n          .procedure\n          .get(&procedure.id)\n          .await\n          .unwrap_or_default()\n          .get()?,\n      ) {\n        (_, action_states) if action_states.running => {\n          res.running += 1;\n        }\n        (ProcedureState::Ok, _) => res.ok += 1,\n        (ProcedureState::Failed, _) => res.failed += 1,\n        (ProcedureState::Unknown, _) => res.unknown += 1,\n        // will never come off the cache in the running state, since that comes from action states\n        (ProcedureState::Running, _) => unreachable!(),\n      }\n    }\n\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetProcedureActionState {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetProcedureActionStateResponse> {\n    let procedure = get_check_permissions::<Procedure>(\n      &self.procedure,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let action_state = action_states()\n      .procedure\n      .get(&procedure.id)\n      .await\n      .unwrap_or_default()\n      .get()?;\n    Ok(action_state)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/provider.rs",
    "content": "use anyhow::{Context, anyhow};\nuse database::mongo_indexed::{Document, doc};\nuse database::mungos::{\n  by_id::find_one_by_id, find::find_collect,\n  mongodb::options::FindOptions,\n};\nuse komodo_client::api::read::*;\nuse resolver_api::Resolve;\n\nuse crate::state::db_client;\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetGitProviderAccount {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetGitProviderAccountResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"Only admins can read git provider accounts\").into(),\n      );\n    }\n    let res = find_one_by_id(&db_client().git_accounts, &self.id)\n      .await\n      .context(\"failed to query db for git provider accounts\")?\n      .context(\n        \"did not find git provider account with the given id\",\n      )?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListGitProviderAccounts {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListGitProviderAccountsResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"Only admins can read git provider accounts\").into(),\n      );\n    }\n    let mut filter = Document::new();\n    if let Some(domain) = self.domain {\n      filter.insert(\"domain\", domain);\n    }\n    if let Some(username) = self.username {\n      filter.insert(\"username\", username);\n    }\n    let res = find_collect(\n      &db_client().git_accounts,\n      filter,\n      FindOptions::builder()\n        .sort(doc! { \"domain\": 1, \"username\": 1 })\n        .build(),\n    )\n    .await\n    .context(\"failed to query db for git provider accounts\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetDockerRegistryAccount {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetDockerRegistryAccountResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"Only admins can read docker registry accounts\")\n          .into(),\n      );\n    }\n    let res =\n      find_one_by_id(&db_client().registry_accounts, &self.id)\n        .await\n        .context(\"failed to query db for docker registry accounts\")?\n        .context(\n          \"did not find docker registry account with the given id\",\n        )?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListDockerRegistryAccounts {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListDockerRegistryAccountsResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"Only admins can read docker registry accounts\")\n          .into(),\n      );\n    }\n    let mut filter = Document::new();\n    if let Some(domain) = self.domain {\n      filter.insert(\"domain\", domain);\n    }\n    if let Some(username) = self.username {\n      filter.insert(\"username\", username);\n    }\n    let res = find_collect(\n      &db_client().registry_accounts,\n      filter,\n      FindOptions::builder()\n        .sort(doc! { \"domain\": 1, \"username\": 1 })\n        .build(),\n    )\n    .await\n    .context(\"failed to query db for docker registry accounts\")?;\n    Ok(res)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/repo.rs",
    "content": "use anyhow::Context;\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    config::core::CoreConfig,\n    permission::PermissionLevel,\n    repo::{Repo, RepoActionState, RepoListItem, RepoState},\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  config::core_config,\n  helpers::query::get_all_tags,\n  permission::get_check_permissions,\n  resource,\n  state::{action_states, github_client, repo_state_cache},\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetRepo {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Repo> {\n    Ok(\n      get_check_permissions::<Repo>(\n        &self.repo,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListRepos {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<RepoListItem>> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_for_user::<Repo>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListFullRepos {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListFullReposResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_full_for_user::<Repo>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for GetRepoActionState {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<RepoActionState> {\n    let repo = get_check_permissions::<Repo>(\n      &self.repo,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let action_state = action_states()\n      .repo\n      .get(&repo.id)\n      .await\n      .unwrap_or_default()\n      .get()?;\n    Ok(action_state)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetReposSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetReposSummaryResponse> {\n    let repos = resource::list_full_for_user::<Repo>(\n      Default::default(),\n      user,\n      PermissionLevel::Read.into(),\n      &[],\n    )\n    .await\n    .context(\"failed to get repos from db\")?;\n\n    let mut res = GetReposSummaryResponse::default();\n\n    let cache = repo_state_cache();\n    let action_states = action_states();\n\n    for repo in repos {\n      res.total += 1;\n\n      match (\n        cache.get(&repo.id).await.unwrap_or_default(),\n        action_states\n          .repo\n          .get(&repo.id)\n          .await\n          .unwrap_or_default()\n          .get()?,\n      ) {\n        (_, action_states) if action_states.cloning => {\n          res.cloning += 1;\n        }\n        (_, action_states) if action_states.pulling => {\n          res.pulling += 1;\n        }\n        (_, action_states) if action_states.building => {\n          res.building += 1;\n        }\n        (RepoState::Ok, _) => res.ok += 1,\n        (RepoState::Failed, _) => res.failed += 1,\n        (RepoState::Unknown, _) => res.unknown += 1,\n        // will never come off the cache in the building state, since that comes from action states\n        (RepoState::Cloning, _)\n        | (RepoState::Pulling, _)\n        | (RepoState::Building, _) => {\n          unreachable!()\n        }\n      }\n    }\n\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetRepoWebhooksEnabled {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetRepoWebhooksEnabledResponse> {\n    let Some(github) = github_client() else {\n      return Ok(GetRepoWebhooksEnabledResponse {\n        managed: false,\n        clone_enabled: false,\n        pull_enabled: false,\n        build_enabled: false,\n      });\n    };\n\n    let repo = get_check_permissions::<Repo>(\n      &self.repo,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n\n    if repo.config.git_provider != \"github.com\"\n      || repo.config.repo.is_empty()\n    {\n      return Ok(GetRepoWebhooksEnabledResponse {\n        managed: false,\n        clone_enabled: false,\n        pull_enabled: false,\n        build_enabled: false,\n      });\n    }\n\n    let mut split = repo.config.repo.split('/');\n    let owner = split.next().context(\"Repo repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Ok(GetRepoWebhooksEnabledResponse {\n        managed: false,\n        clone_enabled: false,\n        pull_enabled: false,\n        build_enabled: false,\n      });\n    };\n\n    let repo_name =\n      split.next().context(\"Repo repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo_name)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      ..\n    } = core_config();\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let clone_url =\n      format!(\"{host}/listener/github/repo/{}/clone\", repo.id);\n    let pull_url =\n      format!(\"{host}/listener/github/repo/{}/pull\", repo.id);\n    let build_url =\n      format!(\"{host}/listener/github/repo/{}/build\", repo.id);\n\n    let mut clone_enabled = false;\n    let mut pull_enabled = false;\n    let mut build_enabled = false;\n\n    for webhook in webhooks {\n      if !webhook.active {\n        continue;\n      }\n      if webhook.config.url == clone_url {\n        clone_enabled = true\n      }\n      if webhook.config.url == pull_url {\n        pull_enabled = true\n      }\n      if webhook.config.url == build_url {\n        build_enabled = true\n      }\n    }\n\n    Ok(GetRepoWebhooksEnabledResponse {\n      managed: true,\n      clone_enabled,\n      pull_enabled,\n      build_enabled,\n    })\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/schedule.rs",
    "content": "use futures::future::join_all;\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    ResourceTarget,\n    action::Action,\n    permission::PermissionLevel,\n    procedure::Procedure,\n    resource::{ResourceQuery, TemplatesQueryBehavior},\n    schedule::Schedule,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::query::{get_all_tags, get_last_run_at},\n  resource::list_full_for_user,\n  schedule::get_schedule_item_info,\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for ListSchedules {\n  async fn resolve(\n    self,\n    args: &ReadArgs,\n  ) -> serror::Result<Vec<Schedule>> {\n    let all_tags = get_all_tags(None).await?;\n    let (actions, procedures) = tokio::try_join!(\n      list_full_for_user::<Action>(\n        ResourceQuery {\n          names: Default::default(),\n          templates: TemplatesQueryBehavior::Include,\n          tag_behavior: self.tag_behavior,\n          tags: self.tags.clone(),\n          specific: Default::default(),\n        },\n        &args.user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      ),\n      list_full_for_user::<Procedure>(\n        ResourceQuery {\n          names: Default::default(),\n          templates: TemplatesQueryBehavior::Include,\n          tag_behavior: self.tag_behavior,\n          tags: self.tags.clone(),\n          specific: Default::default(),\n        },\n        &args.user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n    )?;\n    let actions = actions.into_iter().map(async |action| {\n      let (next_scheduled_run, schedule_error) =\n        get_schedule_item_info(&ResourceTarget::Action(\n          action.id.clone(),\n        ));\n      let last_run_at =\n        get_last_run_at::<Action>(&action.id).await.unwrap_or(None);\n      Schedule {\n        target: ResourceTarget::Action(action.id),\n        name: action.name,\n        enabled: action.config.schedule_enabled,\n        schedule_format: action.config.schedule_format,\n        schedule: action.config.schedule,\n        schedule_timezone: action.config.schedule_timezone,\n        tags: action.tags,\n        last_run_at,\n        next_scheduled_run,\n        schedule_error,\n      }\n    });\n    let procedures = procedures.into_iter().map(async |procedure| {\n      let (next_scheduled_run, schedule_error) =\n        get_schedule_item_info(&ResourceTarget::Procedure(\n          procedure.id.clone(),\n        ));\n      let last_run_at = get_last_run_at::<Procedure>(&procedure.id)\n        .await\n        .unwrap_or(None);\n      Schedule {\n        target: ResourceTarget::Procedure(procedure.id),\n        name: procedure.name,\n        enabled: procedure.config.schedule_enabled,\n        schedule_format: procedure.config.schedule_format,\n        schedule: procedure.config.schedule,\n        schedule_timezone: procedure.config.schedule_timezone,\n        tags: procedure.tags,\n        last_run_at,\n        next_scheduled_run,\n        schedule_error,\n      }\n    });\n    let (actions, procedures) =\n      tokio::join!(join_all(actions), join_all(procedures));\n\n    Ok(\n      actions\n        .into_iter()\n        .chain(procedures)\n        .filter(|s| !s.schedule.is_empty())\n        .collect(),\n    )\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/server.rs",
    "content": "use std::{\n  cmp,\n  collections::HashMap,\n  sync::{Arc, OnceLock},\n};\n\nuse anyhow::{Context, anyhow};\nuse async_timing_util::{\n  FIFTEEN_SECONDS_MS, get_timelength_in_ms, unix_timestamp_ms,\n};\nuse database::mungos::{\n  find::find_collect,\n  mongodb::{bson::doc, options::FindOptions},\n};\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    ResourceTarget,\n    deployment::Deployment,\n    docker::{\n      container::{\n        Container, ContainerListItem, ContainerStateStatusEnum,\n      },\n      image::{Image, ImageHistoryResponseItem},\n      network::Network,\n      volume::Volume,\n    },\n    komodo_timestamp,\n    permission::PermissionLevel,\n    server::{\n      Server, ServerActionState, ServerListItem, ServerState,\n      TerminalInfo,\n    },\n    stack::{Stack, StackServiceNames},\n    stats::{SystemInformation, SystemProcess},\n    update::Log,\n  },\n};\nuse periphery_client::api::{\n  self as periphery,\n  container::InspectContainer,\n  image::{ImageHistory, InspectImage},\n  network::InspectNetwork,\n  volume::InspectVolume,\n};\nuse resolver_api::Resolve;\nuse tokio::sync::Mutex;\n\nuse crate::{\n  helpers::{\n    periphery_client,\n    query::{get_all_tags, get_system_info},\n  },\n  permission::get_check_permissions,\n  resource,\n  stack::compose_container_match_regex,\n  state::{action_states, db_client, server_status_cache},\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetServersSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetServersSummaryResponse> {\n    let servers = resource::list_for_user::<Server>(\n      Default::default(),\n      user,\n      PermissionLevel::Read.into(),\n      &[],\n    )\n    .await?;\n\n    let core_version = env!(\"CARGO_PKG_VERSION\");\n    let mut res = GetServersSummaryResponse::default();\n\n    for server in servers {\n      res.total += 1;\n      match server.info.state {\n        ServerState::Ok => {\n          // Check for version mismatch\n          let has_version_mismatch = !server.info.version.is_empty()\n            && server.info.version != \"Unknown\"\n            && server.info.version != core_version;\n\n          if has_version_mismatch {\n            res.warning += 1;\n          } else {\n            res.healthy += 1;\n          }\n        }\n        ServerState::NotOk => {\n          res.unhealthy += 1;\n        }\n        ServerState::Disabled => {\n          res.disabled += 1;\n        }\n      }\n    }\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetPeripheryVersion {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetPeripheryVersionResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let version = server_status_cache()\n      .get(&server.id)\n      .await\n      .map(|s| s.version.clone())\n      .unwrap_or(String::from(\"unknown\"));\n    Ok(GetPeripheryVersionResponse { version })\n  }\n}\n\nimpl Resolve<ReadArgs> for GetServer {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Server> {\n    Ok(\n      get_check_permissions::<Server>(\n        &self.server,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListServers {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<ServerListItem>> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_for_user::<Server>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListFullServers {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListFullServersResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_full_for_user::<Server>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for GetServerState {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetServerStateResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let status = server_status_cache()\n      .get(&server.id)\n      .await\n      .ok_or(anyhow!(\"did not find cached status for server\"))?;\n    let response = GetServerStateResponse {\n      status: status.state,\n    };\n    Ok(response)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetServerActionState {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ServerActionState> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let action_state = action_states()\n      .server\n      .get(&server.id)\n      .await\n      .unwrap_or_default()\n      .get()?;\n    Ok(action_state)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetSystemInformation {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<SystemInformation> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    get_system_info(&server).await.map_err(Into::into)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetSystemStats {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetSystemStatsResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let status =\n      server_status_cache().get(&server.id).await.with_context(\n        || format!(\"did not find status for server at {}\", server.id),\n      )?;\n    let stats = status\n      .stats\n      .as_ref()\n      .context(\"server stats not available\")?;\n    Ok(stats.clone())\n  }\n}\n\n// This protects the peripheries from spam requests\nconst PROCESSES_EXPIRY: u128 = FIFTEEN_SECONDS_MS;\ntype ProcessesCache =\n  Mutex<HashMap<String, Arc<(Vec<SystemProcess>, u128)>>>;\nfn processes_cache() -> &'static ProcessesCache {\n  static PROCESSES_CACHE: OnceLock<ProcessesCache> = OnceLock::new();\n  PROCESSES_CACHE.get_or_init(Default::default)\n}\n\nimpl Resolve<ReadArgs> for ListSystemProcesses {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListSystemProcessesResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.processes(),\n    )\n    .await?;\n    let mut lock = processes_cache().lock().await;\n    let res = match lock.get(&server.id) {\n      Some(cached) if cached.1 > unix_timestamp_ms() => {\n        cached.0.clone()\n      }\n      _ => {\n        let stats = periphery_client(&server)?\n          .request(periphery::stats::GetSystemProcesses {})\n          .await?;\n        lock.insert(\n          server.id,\n          (stats.clone(), unix_timestamp_ms() + PROCESSES_EXPIRY)\n            .into(),\n        );\n        stats\n      }\n    };\n    Ok(res)\n  }\n}\n\nconst STATS_PER_PAGE: i64 = 200;\n\nimpl Resolve<ReadArgs> for GetHistoricalServerStats {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetHistoricalServerStatsResponse> {\n    let GetHistoricalServerStats {\n      server,\n      granularity,\n      page,\n    } = self;\n    let server = get_check_permissions::<Server>(\n      &server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let granularity =\n      get_timelength_in_ms(granularity.to_string().parse().unwrap())\n        as i64;\n    let mut ts_vec = Vec::<i64>::new();\n    let curr_ts = unix_timestamp_ms() as i64;\n    let mut curr_ts = curr_ts\n      - curr_ts % granularity\n      - granularity * STATS_PER_PAGE * page as i64;\n    for _ in 0..STATS_PER_PAGE {\n      ts_vec.push(curr_ts);\n      curr_ts -= granularity;\n    }\n\n    let stats = find_collect(\n      &db_client().stats,\n      doc! {\n        \"sid\": server.id,\n        \"ts\": { \"$in\": ts_vec },\n      },\n      FindOptions::builder()\n        .sort(doc! { \"ts\": -1 })\n        .skip(page as u64 * STATS_PER_PAGE as u64)\n        .limit(STATS_PER_PAGE)\n        .build(),\n    )\n    .await\n    .context(\"failed to pull stats from db\")?;\n    let next_page = if stats.len() == STATS_PER_PAGE as usize {\n      Some(page + 1)\n    } else {\n      None\n    };\n    let res = GetHistoricalServerStatsResponse { stats, next_page };\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListDockerContainers {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListDockerContainersResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if let Some(containers) = &cache.containers {\n      Ok(containers.clone())\n    } else {\n      Ok(Vec::new())\n    }\n  }\n}\n\nimpl Resolve<ReadArgs> for ListAllDockerContainers {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListAllDockerContainersResponse> {\n    let servers = resource::list_for_user::<Server>(\n      Default::default(),\n      user,\n      PermissionLevel::Read.into(),\n      &[],\n    )\n    .await?\n    .into_iter()\n    .filter(|server| {\n      self.servers.is_empty()\n        || self.servers.contains(&server.id)\n        || self.servers.contains(&server.name)\n    });\n\n    let mut containers = Vec::<ContainerListItem>::new();\n\n    for server in servers {\n      let cache = server_status_cache()\n        .get_or_insert_default(&server.id)\n        .await;\n      if let Some(more_containers) = &cache.containers {\n        containers.extend(more_containers.clone());\n      }\n    }\n\n    Ok(containers)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetDockerContainersSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetDockerContainersSummaryResponse> {\n    let servers = resource::list_full_for_user::<Server>(\n      Default::default(),\n      user,\n      PermissionLevel::Read.into(),\n      &[],\n    )\n    .await\n    .context(\"failed to get servers from db\")?;\n\n    let mut res = GetDockerContainersSummaryResponse::default();\n\n    for server in servers {\n      let cache = server_status_cache()\n        .get_or_insert_default(&server.id)\n        .await;\n\n      if let Some(containers) = &cache.containers {\n        for container in containers {\n          res.total += 1;\n          match container.state {\n            ContainerStateStatusEnum::Created\n            | ContainerStateStatusEnum::Paused\n            | ContainerStateStatusEnum::Exited => res.stopped += 1,\n            ContainerStateStatusEnum::Running => res.running += 1,\n            ContainerStateStatusEnum::Empty => res.unknown += 1,\n            _ => res.unhealthy += 1,\n          }\n        }\n      }\n    }\n\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for InspectDockerContainer {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Container> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.inspect(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if cache.state != ServerState::Ok {\n      return Err(\n        anyhow!(\n          \"Cannot inspect container: server is {:?}\",\n          cache.state\n        )\n        .into(),\n      );\n    }\n    let res = periphery_client(&server)?\n      .request(InspectContainer {\n        name: self.container,\n      })\n      .await?;\n    Ok(res)\n  }\n}\n\nconst MAX_LOG_LENGTH: u64 = 5000;\n\nimpl Resolve<ReadArgs> for GetContainerLog {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Log> {\n    let GetContainerLog {\n      server,\n      container,\n      tail,\n      timestamps,\n    } = self;\n    let server = get_check_permissions::<Server>(\n      &server,\n      user,\n      PermissionLevel::Read.logs(),\n    )\n    .await?;\n    let res = periphery_client(&server)?\n      .request(periphery::container::GetContainerLog {\n        name: container,\n        tail: cmp::min(tail, MAX_LOG_LENGTH),\n        timestamps,\n      })\n      .await\n      .context(\"failed at call to periphery\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for SearchContainerLog {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Log> {\n    let SearchContainerLog {\n      server,\n      container,\n      terms,\n      combinator,\n      invert,\n      timestamps,\n    } = self;\n    let server = get_check_permissions::<Server>(\n      &server,\n      user,\n      PermissionLevel::Read.logs(),\n    )\n    .await?;\n    let res = periphery_client(&server)?\n      .request(periphery::container::GetContainerLogSearch {\n        name: container,\n        terms,\n        combinator,\n        invert,\n        timestamps,\n      })\n      .await\n      .context(\"failed at call to periphery\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetResourceMatchingContainer {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetResourceMatchingContainerResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    // first check deployments\n    if let Ok(deployment) =\n      resource::get::<Deployment>(&self.container).await\n    {\n      return Ok(GetResourceMatchingContainerResponse {\n        resource: ResourceTarget::Deployment(deployment.id).into(),\n      });\n    }\n\n    // then check stacks\n    let stacks =\n      resource::list_full_for_user_using_document::<Stack>(\n        doc! { \"config.server_id\": &server.id },\n        user,\n      )\n      .await?;\n\n    // check matching stack\n    for stack in stacks {\n      for StackServiceNames {\n        service_name,\n        container_name,\n        ..\n      } in stack\n        .info\n        .deployed_services\n        .unwrap_or(stack.info.latest_services)\n      {\n        let is_match = match compose_container_match_regex(&container_name)\n          .with_context(|| format!(\"failed to construct container name matching regex for service {service_name}\")) \n        {\n          Ok(regex) => regex,\n          Err(e) => {\n            warn!(\"{e:#}\");\n            continue;\n          }\n        }.is_match(&self.container);\n\n        if is_match {\n          return Ok(GetResourceMatchingContainerResponse {\n            resource: ResourceTarget::Stack(stack.id).into(),\n          });\n        }\n      }\n    }\n\n    Ok(GetResourceMatchingContainerResponse { resource: None })\n  }\n}\n\nimpl Resolve<ReadArgs> for ListDockerNetworks {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListDockerNetworksResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if let Some(networks) = &cache.networks {\n      Ok(networks.clone())\n    } else {\n      Ok(Vec::new())\n    }\n  }\n}\n\nimpl Resolve<ReadArgs> for InspectDockerNetwork {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Network> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if cache.state != ServerState::Ok {\n      return Err(\n        anyhow!(\n          \"Cannot inspect network: server is {:?}\",\n          cache.state\n        )\n        .into(),\n      );\n    }\n    let res = periphery_client(&server)?\n      .request(InspectNetwork { name: self.network })\n      .await?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListDockerImages {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListDockerImagesResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if let Some(images) = &cache.images {\n      Ok(images.clone())\n    } else {\n      Ok(Vec::new())\n    }\n  }\n}\n\nimpl Resolve<ReadArgs> for InspectDockerImage {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Image> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if cache.state != ServerState::Ok {\n      return Err(\n        anyhow!(\"Cannot inspect image: server is {:?}\", cache.state)\n          .into(),\n      );\n    }\n    let res = periphery_client(&server)?\n      .request(InspectImage { name: self.image })\n      .await?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListDockerImageHistory {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<ImageHistoryResponseItem>> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if cache.state != ServerState::Ok {\n      return Err(\n        anyhow!(\n          \"Cannot get image history: server is {:?}\",\n          cache.state\n        )\n        .into(),\n      );\n    }\n    let res = periphery_client(&server)?\n      .request(ImageHistory { name: self.image })\n      .await?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListDockerVolumes {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListDockerVolumesResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if let Some(volumes) = &cache.volumes {\n      Ok(volumes.clone())\n    } else {\n      Ok(Vec::new())\n    }\n  }\n}\n\nimpl Resolve<ReadArgs> for InspectDockerVolume {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Volume> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if cache.state != ServerState::Ok {\n      return Err(\n        anyhow!(\"Cannot inspect volume: server is {:?}\", cache.state)\n          .into(),\n      );\n    }\n    let res = periphery_client(&server)?\n      .request(InspectVolume { name: self.volume })\n      .await?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListComposeProjects {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListComposeProjectsResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if let Some(projects) = &cache.projects {\n      Ok(projects.clone())\n    } else {\n      Ok(Vec::new())\n    }\n  }\n}\n\n#[derive(Default)]\nstruct TerminalCacheItem {\n  list: Vec<TerminalInfo>,\n  ttl: i64,\n}\n\nconst TERMINAL_CACHE_TIMEOUT: i64 = 30_000;\n\n#[derive(Default)]\nstruct TerminalCache(\n  std::sync::Mutex<\n    HashMap<String, Arc<tokio::sync::Mutex<TerminalCacheItem>>>,\n  >,\n);\n\nimpl TerminalCache {\n  fn get_or_insert(\n    &self,\n    server_id: String,\n  ) -> Arc<tokio::sync::Mutex<TerminalCacheItem>> {\n    if let Some(cached) =\n      self.0.lock().unwrap().get(&server_id).cloned()\n    {\n      return cached;\n    }\n    let to_cache =\n      Arc::new(tokio::sync::Mutex::new(TerminalCacheItem::default()));\n    self.0.lock().unwrap().insert(server_id, to_cache.clone());\n    to_cache\n  }\n}\n\nfn terminals_cache() -> &'static TerminalCache {\n  static TERMINALS: OnceLock<TerminalCache> = OnceLock::new();\n  TERMINALS.get_or_init(Default::default)\n}\n\nimpl Resolve<ReadArgs> for ListTerminals {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListTerminalsResponse> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.terminal(),\n    )\n    .await?;\n    let cache = terminals_cache().get_or_insert(server.id.clone());\n    let mut cache = cache.lock().await;\n    if self.fresh || komodo_timestamp() > cache.ttl {\n      cache.list = periphery_client(&server)?\n        .request(periphery_client::api::terminal::ListTerminals {})\n        .await\n        .context(\"Failed to get fresh terminal list\")?;\n      cache.ttl = komodo_timestamp() + TERMINAL_CACHE_TIMEOUT;\n      Ok(cache.list.clone())\n    } else {\n      Ok(cache.list.clone())\n    }\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/stack.rs",
    "content": "use std::collections::HashSet;\n\nuse anyhow::{Context, anyhow};\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    config::core::CoreConfig,\n    docker::container::Container,\n    permission::PermissionLevel,\n    server::{Server, ServerState},\n    stack::{Stack, StackActionState, StackListItem, StackState},\n  },\n};\nuse periphery_client::api::{\n  compose::{GetComposeLog, GetComposeLogSearch},\n  container::InspectContainer,\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  config::core_config,\n  helpers::{periphery_client, query::get_all_tags},\n  permission::get_check_permissions,\n  resource,\n  stack::get_stack_and_server,\n  state::{\n    action_states, github_client, server_status_cache,\n    stack_status_cache,\n  },\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetStack {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Stack> {\n    Ok(\n      get_check_permissions::<Stack>(\n        &self.stack,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListStackServices {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListStackServicesResponse> {\n    let stack = get_check_permissions::<Stack>(\n      &self.stack,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n\n    let services = stack_status_cache()\n      .get(&stack.id)\n      .await\n      .unwrap_or_default()\n      .curr\n      .services\n      .clone();\n\n    Ok(services)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetStackLog {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetStackLogResponse> {\n    let GetStackLog {\n      stack,\n      services,\n      tail,\n      timestamps,\n    } = self;\n    let (stack, server) = get_stack_and_server(\n      &stack,\n      user,\n      PermissionLevel::Read.logs(),\n      true,\n    )\n    .await?;\n    let res = periphery_client(&server)?\n      .request(GetComposeLog {\n        project: stack.project_name(false),\n        services,\n        tail,\n        timestamps,\n      })\n      .await\n      .context(\"Failed to get stack log from periphery\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for SearchStackLog {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<SearchStackLogResponse> {\n    let SearchStackLog {\n      stack,\n      services,\n      terms,\n      combinator,\n      invert,\n      timestamps,\n    } = self;\n    let (stack, server) = get_stack_and_server(\n      &stack,\n      user,\n      PermissionLevel::Read.logs(),\n      true,\n    )\n    .await?;\n    let res = periphery_client(&server)?\n      .request(GetComposeLogSearch {\n        project: stack.project_name(false),\n        services,\n        terms,\n        combinator,\n        invert,\n        timestamps,\n      })\n      .await\n      .context(\"Failed to search stack log from periphery\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for InspectStackContainer {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Container> {\n    let InspectStackContainer { stack, service } = self;\n    let stack = get_check_permissions::<Stack>(\n      &stack,\n      user,\n      PermissionLevel::Read.inspect(),\n    )\n    .await?;\n    if stack.config.server_id.is_empty() {\n      return Err(\n        anyhow!(\"Cannot inspect stack, not attached to any server\")\n          .into(),\n      );\n    }\n    let server =\n      resource::get::<Server>(&stack.config.server_id).await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if cache.state != ServerState::Ok {\n      return Err(\n        anyhow!(\n          \"Cannot inspect container: server is {:?}\",\n          cache.state\n        )\n        .into(),\n      );\n    }\n    let services = &stack_status_cache()\n      .get(&stack.id)\n      .await\n      .unwrap_or_default()\n      .curr\n      .services;\n    let Some(name) = services\n      .iter()\n      .find(|s| s.service == service)\n      .and_then(|s| s.container.as_ref().map(|c| c.name.clone()))\n    else {\n      return Err(anyhow!(\n        \"No service found matching '{service}'. Was the stack last deployed manually?\"\n      ).into());\n    };\n    let res = periphery_client(&server)?\n      .request(InspectContainer { name })\n      .await?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListCommonStackExtraArgs {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListCommonStackExtraArgsResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    let stacks = resource::list_full_for_user::<Stack>(\n      self.query,\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await\n    .context(\"failed to get resources matching query\")?;\n\n    // first collect with guaranteed uniqueness\n    let mut res = HashSet::<String>::new();\n\n    for stack in stacks {\n      for extra_arg in stack.config.extra_args {\n        res.insert(extra_arg);\n      }\n    }\n\n    let mut res = res.into_iter().collect::<Vec<_>>();\n    res.sort();\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListCommonStackBuildExtraArgsResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    let stacks = resource::list_full_for_user::<Stack>(\n      self.query,\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await\n    .context(\"failed to get resources matching query\")?;\n\n    // first collect with guaranteed uniqueness\n    let mut res = HashSet::<String>::new();\n\n    for stack in stacks {\n      for extra_arg in stack.config.build_extra_args {\n        res.insert(extra_arg);\n      }\n    }\n\n    let mut res = res.into_iter().collect::<Vec<_>>();\n    res.sort();\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListStacks {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<StackListItem>> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    let only_update_available = self.query.specific.update_available;\n    let stacks = resource::list_for_user::<Stack>(\n      self.query,\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?;\n    let stacks = if only_update_available {\n      stacks\n        .into_iter()\n        .filter(|stack| {\n          stack\n            .info\n            .services\n            .iter()\n            .any(|service| service.update_available)\n        })\n        .collect()\n    } else {\n      stacks\n    };\n    Ok(stacks)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListFullStacks {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListFullStacksResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_full_for_user::<Stack>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for GetStackActionState {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<StackActionState> {\n    let stack = get_check_permissions::<Stack>(\n      &self.stack,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let action_state = action_states()\n      .stack\n      .get(&stack.id)\n      .await\n      .unwrap_or_default()\n      .get()?;\n    Ok(action_state)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetStacksSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetStacksSummaryResponse> {\n    let stacks = resource::list_full_for_user::<Stack>(\n      Default::default(),\n      user,\n      PermissionLevel::Read.into(),\n      &[],\n    )\n    .await\n    .context(\"failed to get stacks from db\")?;\n\n    let mut res = GetStacksSummaryResponse::default();\n\n    let cache = stack_status_cache();\n\n    for stack in stacks {\n      res.total += 1;\n      match cache.get(&stack.id).await.unwrap_or_default().curr.state\n      {\n        StackState::Running => res.running += 1,\n        StackState::Stopped | StackState::Paused => res.stopped += 1,\n        StackState::Down => res.down += 1,\n        StackState::Unknown => res.unknown += 1,\n        _ => res.unhealthy += 1,\n      }\n    }\n\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetStackWebhooksEnabled {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetStackWebhooksEnabledResponse> {\n    let Some(github) = github_client() else {\n      return Ok(GetStackWebhooksEnabledResponse {\n        managed: false,\n        refresh_enabled: false,\n        deploy_enabled: false,\n      });\n    };\n\n    let stack = get_check_permissions::<Stack>(\n      &self.stack,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n\n    if stack.config.git_provider != \"github.com\"\n      || stack.config.repo.is_empty()\n    {\n      return Ok(GetStackWebhooksEnabledResponse {\n        managed: false,\n        refresh_enabled: false,\n        deploy_enabled: false,\n      });\n    }\n\n    let mut split = stack.config.repo.split('/');\n    let owner = split.next().context(\"Sync repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Ok(GetStackWebhooksEnabledResponse {\n        managed: false,\n        refresh_enabled: false,\n        deploy_enabled: false,\n      });\n    };\n\n    let repo_name =\n      split.next().context(\"Repo repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo_name)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      ..\n    } = core_config();\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let refresh_url =\n      format!(\"{host}/listener/github/stack/{}/refresh\", stack.id);\n    let deploy_url =\n      format!(\"{host}/listener/github/stack/{}/deploy\", stack.id);\n\n    let mut refresh_enabled = false;\n    let mut deploy_enabled = false;\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == refresh_url {\n        refresh_enabled = true\n      }\n      if webhook.active && webhook.config.url == deploy_url {\n        deploy_enabled = true\n      }\n    }\n\n    Ok(GetStackWebhooksEnabledResponse {\n      managed: true,\n      refresh_enabled,\n      deploy_enabled,\n    })\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/sync.rs",
    "content": "use anyhow::Context;\nuse komodo_client::{\n  api::read::*,\n  entities::{\n    config::core::CoreConfig,\n    permission::PermissionLevel,\n    sync::{\n      ResourceSync, ResourceSyncActionState, ResourceSyncListItem,\n    },\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  config::core_config,\n  helpers::query::get_all_tags,\n  permission::get_check_permissions,\n  resource,\n  state::{action_states, github_client},\n};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetResourceSync {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ResourceSync> {\n    Ok(\n      get_check_permissions::<ResourceSync>(\n        &self.sync,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListResourceSyncs {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Vec<ResourceSyncListItem>> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_for_user::<ResourceSync>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for ListFullResourceSyncs {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListFullResourceSyncsResponse> {\n    let all_tags = if self.query.tags.is_empty() {\n      vec![]\n    } else {\n      get_all_tags(None).await?\n    };\n    Ok(\n      resource::list_full_for_user::<ResourceSync>(\n        self.query,\n        user,\n        PermissionLevel::Read.into(),\n        &all_tags,\n      )\n      .await?,\n    )\n  }\n}\n\nimpl Resolve<ReadArgs> for GetResourceSyncActionState {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ResourceSyncActionState> {\n    let sync = get_check_permissions::<ResourceSync>(\n      &self.sync,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    let action_state = action_states()\n      .sync\n      .get(&sync.id)\n      .await\n      .unwrap_or_default()\n      .get()?;\n    Ok(action_state)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetResourceSyncsSummary {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetResourceSyncsSummaryResponse> {\n    let resource_syncs =\n      resource::list_full_for_user::<ResourceSync>(\n        Default::default(),\n        user,\n        PermissionLevel::Read.into(),\n        &[],\n      )\n      .await\n      .context(\"failed to get resource_syncs from db\")?;\n\n    let mut res = GetResourceSyncsSummaryResponse::default();\n\n    let action_states = action_states();\n\n    for resource_sync in resource_syncs {\n      res.total += 1;\n\n      if !(resource_sync.info.pending_deploy.to_deploy == 0\n        && resource_sync.info.resource_updates.is_empty()\n        && resource_sync.info.variable_updates.is_empty()\n        && resource_sync.info.user_group_updates.is_empty())\n      {\n        res.pending += 1;\n        continue;\n      } else if resource_sync.info.pending_error.is_some()\n        || !resource_sync.info.remote_errors.is_empty()\n      {\n        res.failed += 1;\n        continue;\n      }\n      if action_states\n        .sync\n        .get(&resource_sync.id)\n        .await\n        .unwrap_or_default()\n        .get()?\n        .syncing\n      {\n        res.syncing += 1;\n        continue;\n      }\n      res.ok += 1;\n    }\n\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for GetSyncWebhooksEnabled {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetSyncWebhooksEnabledResponse> {\n    let Some(github) = github_client() else {\n      return Ok(GetSyncWebhooksEnabledResponse {\n        managed: false,\n        refresh_enabled: false,\n        sync_enabled: false,\n      });\n    };\n\n    let sync = get_check_permissions::<ResourceSync>(\n      &self.sync,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n\n    if sync.config.git_provider != \"github.com\"\n      || sync.config.repo.is_empty()\n    {\n      return Ok(GetSyncWebhooksEnabledResponse {\n        managed: false,\n        refresh_enabled: false,\n        sync_enabled: false,\n      });\n    }\n\n    let mut split = sync.config.repo.split('/');\n    let owner = split.next().context(\"Sync repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Ok(GetSyncWebhooksEnabledResponse {\n        managed: false,\n        refresh_enabled: false,\n        sync_enabled: false,\n      });\n    };\n\n    let repo_name =\n      split.next().context(\"Repo repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo_name)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      ..\n    } = core_config();\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let refresh_url =\n      format!(\"{host}/listener/github/sync/{}/refresh\", sync.id);\n    let sync_url =\n      format!(\"{host}/listener/github/sync/{}/sync\", sync.id);\n\n    let mut refresh_enabled = false;\n    let mut sync_enabled = false;\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == refresh_url {\n        refresh_enabled = true\n      }\n      if webhook.active && webhook.config.url == sync_url {\n        sync_enabled = true\n      }\n    }\n\n    Ok(GetSyncWebhooksEnabledResponse {\n      managed: true,\n      refresh_enabled,\n      sync_enabled,\n    })\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/tag.rs",
    "content": "use anyhow::Context;\nuse database::mongo_indexed::doc;\nuse database::mungos::{\n  find::find_collect, mongodb::options::FindOptions,\n};\nuse komodo_client::{\n  api::read::{GetTag, ListTags},\n  entities::tag::Tag,\n};\nuse resolver_api::Resolve;\n\nuse crate::{helpers::query::get_tag, state::db_client};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetTag {\n  async fn resolve(self, _: &ReadArgs) -> serror::Result<Tag> {\n    Ok(get_tag(&self.tag).await?)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListTags {\n  async fn resolve(self, _: &ReadArgs) -> serror::Result<Vec<Tag>> {\n    let res = find_collect(\n      &db_client().tags,\n      self.query,\n      FindOptions::builder().sort(doc! { \"name\": 1 }).build(),\n    )\n    .await\n    .context(\"failed to get tags from db\")?;\n    Ok(res)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/toml.rs",
    "content": "use anyhow::Context;\nuse database::mungos::find::find_collect;\nuse komodo_client::{\n  api::read::{\n    ExportAllResourcesToToml, ExportAllResourcesToTomlResponse,\n    ExportResourcesToToml, ExportResourcesToTomlResponse,\n    ListUserGroups,\n  },\n  entities::{\n    ResourceTarget, action::Action, alerter::Alerter, build::Build,\n    builder::Builder, deployment::Deployment,\n    permission::PermissionLevel, procedure::Procedure, repo::Repo,\n    resource::ResourceQuery, server::Server, stack::Stack,\n    sync::ResourceSync, toml::ResourcesToml, user::User,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::query::{\n    get_all_tags, get_id_to_tags, get_user_user_group_ids,\n  },\n  permission::get_check_permissions,\n  resource,\n  state::db_client,\n  sync::{\n    toml::{ToToml, convert_resource},\n    user_groups::{convert_user_groups, user_group_to_toml},\n    variables::variable_to_toml,\n  },\n};\n\nuse super::ReadArgs;\n\nasync fn get_all_targets(\n  tags: &[String],\n  user: &User,\n) -> anyhow::Result<Vec<ResourceTarget>> {\n  let mut targets = Vec::<ResourceTarget>::new();\n  let all_tags = if tags.is_empty() {\n    vec![]\n  } else {\n    get_all_tags(None).await?\n  };\n  targets.extend(\n    resource::list_full_for_user::<Alerter>(\n      ResourceQuery::builder().tags(tags).build(),\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?\n    .into_iter()\n    .map(|resource| ResourceTarget::Alerter(resource.id)),\n  );\n  targets.extend(\n    resource::list_full_for_user::<Builder>(\n      ResourceQuery::builder().tags(tags).build(),\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?\n    .into_iter()\n    .map(|resource| ResourceTarget::Builder(resource.id)),\n  );\n  targets.extend(\n    resource::list_full_for_user::<Server>(\n      ResourceQuery::builder().tags(tags).build(),\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?\n    .into_iter()\n    .map(|resource| ResourceTarget::Server(resource.id)),\n  );\n  targets.extend(\n    resource::list_full_for_user::<Stack>(\n      ResourceQuery::builder().tags(tags).build(),\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?\n    .into_iter()\n    .map(|resource| ResourceTarget::Stack(resource.id)),\n  );\n  targets.extend(\n    resource::list_full_for_user::<Deployment>(\n      ResourceQuery::builder().tags(tags).build(),\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?\n    .into_iter()\n    .map(|resource| ResourceTarget::Deployment(resource.id)),\n  );\n  targets.extend(\n    resource::list_full_for_user::<Build>(\n      ResourceQuery::builder().tags(tags).build(),\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?\n    .into_iter()\n    .map(|resource| ResourceTarget::Build(resource.id)),\n  );\n  targets.extend(\n    resource::list_full_for_user::<Repo>(\n      ResourceQuery::builder().tags(tags).build(),\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?\n    .into_iter()\n    .map(|resource| ResourceTarget::Repo(resource.id)),\n  );\n  targets.extend(\n    resource::list_full_for_user::<Procedure>(\n      ResourceQuery::builder().tags(tags).build(),\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?\n    .into_iter()\n    .map(|resource| ResourceTarget::Procedure(resource.id)),\n  );\n  targets.extend(\n    resource::list_full_for_user::<Action>(\n      ResourceQuery::builder().tags(tags).build(),\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?\n    .into_iter()\n    .map(|resource| ResourceTarget::Action(resource.id)),\n  );\n  targets.extend(\n    resource::list_full_for_user::<ResourceSync>(\n      ResourceQuery::builder().tags(tags).build(),\n      user,\n      PermissionLevel::Read.into(),\n      &all_tags,\n    )\n    .await?\n    .into_iter()\n    // These will already be filtered by [ExportResourcesToToml]\n    .map(|resource| ResourceTarget::ResourceSync(resource.id)),\n  );\n  Ok(targets)\n}\n\nimpl Resolve<ReadArgs> for ExportAllResourcesToToml {\n  async fn resolve(\n    self,\n    args: &ReadArgs,\n  ) -> serror::Result<ExportAllResourcesToTomlResponse> {\n    let targets = if self.include_resources {\n      get_all_targets(&self.tags, &args.user).await?\n    } else {\n      Vec::new()\n    };\n\n    let user_groups = if self.include_user_groups {\n      if args.user.admin {\n        find_collect(&db_client().user_groups, None, None)\n          .await\n          .context(\"failed to query db for user groups\")?\n          .into_iter()\n          .map(|user_group| user_group.id)\n          .collect()\n      } else {\n        get_user_user_group_ids(&args.user.id).await?\n      }\n    } else {\n      Vec::new()\n    };\n\n    ExportResourcesToToml {\n      targets,\n      user_groups,\n      include_variables: self.include_variables,\n    }\n    .resolve(args)\n    .await\n  }\n}\n\nimpl Resolve<ReadArgs> for ExportResourcesToToml {\n  async fn resolve(\n    self,\n    args: &ReadArgs,\n  ) -> serror::Result<ExportResourcesToTomlResponse> {\n    let ExportResourcesToToml {\n      targets,\n      user_groups,\n      include_variables,\n    } = self;\n    let mut res = ResourcesToml::default();\n    let id_to_tags = get_id_to_tags(None).await?;\n    let ReadArgs { user } = args;\n    for target in targets {\n      match target {\n        ResourceTarget::Alerter(id) => {\n          let mut alerter = get_check_permissions::<Alerter>(\n            &id,\n            user,\n            PermissionLevel::Read.into(),\n          )\n          .await?;\n          Alerter::replace_ids(&mut alerter);\n          res.alerters.push(convert_resource::<Alerter>(\n            alerter,\n            false,\n            vec![],\n            &id_to_tags,\n          ))\n        }\n        ResourceTarget::ResourceSync(id) => {\n          let mut sync = get_check_permissions::<ResourceSync>(\n            &id,\n            user,\n            PermissionLevel::Read.into(),\n          )\n          .await?;\n          if sync.config.file_contents.is_empty()\n            && (sync.config.files_on_host\n              || !sync.config.repo.is_empty()\n              || !sync.config.linked_repo.is_empty())\n          {\n            ResourceSync::replace_ids(&mut sync);\n            res.resource_syncs.push(convert_resource::<ResourceSync>(\n              sync,\n              false,\n              vec![],\n              &id_to_tags,\n            ))\n          }\n        }\n        ResourceTarget::Server(id) => {\n          let mut server = get_check_permissions::<Server>(\n            &id,\n            user,\n            PermissionLevel::Read.into(),\n          )\n          .await?;\n          Server::replace_ids(&mut server);\n          res.servers.push(convert_resource::<Server>(\n            server,\n            false,\n            vec![],\n            &id_to_tags,\n          ))\n        }\n        ResourceTarget::Builder(id) => {\n          let mut builder = get_check_permissions::<Builder>(\n            &id,\n            user,\n            PermissionLevel::Read.into(),\n          )\n          .await?;\n          Builder::replace_ids(&mut builder);\n          res.builders.push(convert_resource::<Builder>(\n            builder,\n            false,\n            vec![],\n            &id_to_tags,\n          ))\n        }\n        ResourceTarget::Build(id) => {\n          let mut build = get_check_permissions::<Build>(\n            &id,\n            user,\n            PermissionLevel::Read.into(),\n          )\n          .await?;\n          Build::replace_ids(&mut build);\n          res.builds.push(convert_resource::<Build>(\n            build,\n            false,\n            vec![],\n            &id_to_tags,\n          ))\n        }\n        ResourceTarget::Deployment(id) => {\n          let mut deployment = get_check_permissions::<Deployment>(\n            &id,\n            user,\n            PermissionLevel::Read.into(),\n          )\n          .await?;\n          Deployment::replace_ids(&mut deployment);\n          res.deployments.push(convert_resource::<Deployment>(\n            deployment,\n            false,\n            vec![],\n            &id_to_tags,\n          ))\n        }\n        ResourceTarget::Repo(id) => {\n          let mut repo = get_check_permissions::<Repo>(\n            &id,\n            user,\n            PermissionLevel::Read.into(),\n          )\n          .await?;\n          Repo::replace_ids(&mut repo);\n          res.repos.push(convert_resource::<Repo>(\n            repo,\n            false,\n            vec![],\n            &id_to_tags,\n          ))\n        }\n        ResourceTarget::Stack(id) => {\n          let mut stack = get_check_permissions::<Stack>(\n            &id,\n            user,\n            PermissionLevel::Read.into(),\n          )\n          .await?;\n          Stack::replace_ids(&mut stack);\n          res.stacks.push(convert_resource::<Stack>(\n            stack,\n            false,\n            vec![],\n            &id_to_tags,\n          ))\n        }\n        ResourceTarget::Procedure(id) => {\n          let mut procedure = get_check_permissions::<Procedure>(\n            &id,\n            user,\n            PermissionLevel::Read.into(),\n          )\n          .await?;\n          Procedure::replace_ids(&mut procedure);\n          res.procedures.push(convert_resource::<Procedure>(\n            procedure,\n            false,\n            vec![],\n            &id_to_tags,\n          ));\n        }\n        ResourceTarget::Action(id) => {\n          let mut action = get_check_permissions::<Action>(\n            &id,\n            user,\n            PermissionLevel::Read.into(),\n          )\n          .await?;\n          Action::replace_ids(&mut action);\n          res.actions.push(convert_resource::<Action>(\n            action,\n            false,\n            vec![],\n            &id_to_tags,\n          ));\n        }\n        ResourceTarget::System(_) => continue,\n      };\n    }\n\n    add_user_groups(user_groups, &mut res, args)\n      .await\n      .context(\"failed to add user groups\")?;\n\n    if include_variables {\n      res.variables =\n        find_collect(&db_client().variables, None, None)\n          .await\n          .context(\"failed to get variables from db\")?\n          .into_iter()\n          .map(|mut variable| {\n            if !user.admin && variable.is_secret {\n              variable.value = \"#\".repeat(variable.value.len())\n            }\n            variable\n          })\n          .collect();\n    }\n\n    let toml = serialize_resources_toml(res)\n      .context(\"failed to serialize resources to toml\")?;\n\n    Ok(ExportResourcesToTomlResponse { toml })\n  }\n}\n\nasync fn add_user_groups(\n  user_groups: Vec<String>,\n  res: &mut ResourcesToml,\n  args: &ReadArgs,\n) -> anyhow::Result<()> {\n  let user_groups = ListUserGroups {}\n    .resolve(args)\n    .await\n    .map_err(|e| e.error)?\n    .into_iter()\n    .filter(|ug| {\n      user_groups.contains(&ug.name) || user_groups.contains(&ug.id)\n    });\n  let mut ug = Vec::with_capacity(user_groups.size_hint().0);\n  convert_user_groups(user_groups, &mut ug).await?;\n  res.user_groups = ug.into_iter().map(|ug| ug.1).collect();\n\n  Ok(())\n}\n\nfn serialize_resources_toml(\n  resources: ResourcesToml,\n) -> anyhow::Result<String> {\n  let mut toml = String::new();\n\n  for server in resources.servers {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(\"[[server]]\\n\");\n    Server::push_to_toml_string(server, &mut toml)?;\n  }\n\n  for stack in resources.stacks {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(\"[[stack]]\\n\");\n    Stack::push_to_toml_string(stack, &mut toml)?;\n  }\n\n  for deployment in resources.deployments {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(\"[[deployment]]\\n\");\n    Deployment::push_to_toml_string(deployment, &mut toml)?;\n  }\n\n  for build in resources.builds {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(\"[[build]]\\n\");\n    Build::push_to_toml_string(build, &mut toml)?;\n  }\n\n  for repo in resources.repos {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(\"[[repo]]\\n\");\n    Repo::push_to_toml_string(repo, &mut toml)?;\n  }\n\n  for procedure in resources.procedures {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(\"[[procedure]]\\n\");\n    Procedure::push_to_toml_string(procedure, &mut toml)?;\n  }\n\n  for action in resources.actions {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(\"[[action]]\\n\");\n    Action::push_to_toml_string(action, &mut toml)?;\n  }\n\n  for alerter in resources.alerters {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(\"[[alerter]]\\n\");\n    Alerter::push_to_toml_string(alerter, &mut toml)?;\n  }\n\n  for builder in resources.builders {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(\"[[builder]]\\n\");\n    Builder::push_to_toml_string(builder, &mut toml)?;\n  }\n\n  for resource_sync in resources.resource_syncs {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(\"[[resource_sync]]\\n\");\n    ResourceSync::push_to_toml_string(resource_sync, &mut toml)?;\n  }\n\n  for variable in &resources.variables {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(&variable_to_toml(variable)?);\n  }\n\n  for user_group in resources.user_groups {\n    if !toml.is_empty() {\n      toml.push_str(\"\\n\\n##\\n\\n\");\n    }\n    toml.push_str(&user_group_to_toml(user_group)?);\n  }\n\n  Ok(toml)\n}\n"
  },
  {
    "path": "bin/core/src/api/read/update.rs",
    "content": "use std::collections::HashMap;\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::find_one_by_id,\n  find::find_collect,\n  mongodb::{bson::doc, options::FindOptions},\n};\nuse komodo_client::{\n  api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},\n  entities::{\n    ResourceTarget,\n    action::Action,\n    alerter::Alerter,\n    build::Build,\n    builder::Builder,\n    deployment::Deployment,\n    permission::PermissionLevel,\n    procedure::Procedure,\n    repo::Repo,\n    server::Server,\n    stack::Stack,\n    sync::ResourceSync,\n    update::{Update, UpdateListItem},\n    user::User,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  config::core_config,\n  permission::{get_check_permissions, get_resource_ids_for_user},\n  state::db_client,\n};\n\nuse super::ReadArgs;\n\nconst UPDATES_PER_PAGE: i64 = 100;\n\nimpl Resolve<ReadArgs> for ListUpdates {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListUpdatesResponse> {\n    let query = if user.admin || core_config().transparent_mode {\n      self.query\n    } else {\n      let server_query = get_resource_ids_for_user::<Server>(user)\n        .await?\n        .map(|ids| {\n          doc! {\n            \"target.type\": \"Server\", \"target.id\": { \"$in\": ids }\n          }\n        })\n        .unwrap_or_else(|| doc! { \"target.type\": \"Server\" });\n\n      let deployment_query =\n        get_resource_ids_for_user::<Deployment>(user)\n          .await?\n          .map(|ids| {\n            doc! {\n              \"target.type\": \"Deployment\", \"target.id\": { \"$in\": ids }\n            }\n          })\n          .unwrap_or_else(|| doc! { \"target.type\": \"Deployment\" });\n\n      let stack_query = get_resource_ids_for_user::<Stack>(user)\n        .await?\n        .map(|ids| {\n          doc! {\n            \"target.type\": \"Stack\", \"target.id\": { \"$in\": ids }\n          }\n        })\n        .unwrap_or_else(|| doc! { \"target.type\": \"Stack\" });\n\n      let build_query = get_resource_ids_for_user::<Build>(user)\n        .await?\n        .map(|ids| {\n          doc! {\n            \"target.type\": \"Build\", \"target.id\": { \"$in\": ids }\n          }\n        })\n        .unwrap_or_else(|| doc! { \"target.type\": \"Build\" });\n\n      let repo_query = get_resource_ids_for_user::<Repo>(user)\n        .await?\n        .map(|ids| {\n          doc! {\n            \"target.type\": \"Repo\", \"target.id\": { \"$in\": ids }\n          }\n        })\n        .unwrap_or_else(|| doc! { \"target.type\": \"Repo\" });\n\n      let procedure_query =\n        get_resource_ids_for_user::<Procedure>(user)\n          .await?\n          .map(|ids| {\n            doc! {\n              \"target.type\": \"Procedure\", \"target.id\": { \"$in\": ids }\n            }\n          })\n          .unwrap_or_else(|| doc! { \"target.type\": \"Procedure\" });\n\n      let action_query = get_resource_ids_for_user::<Action>(user)\n        .await?\n        .map(|ids| {\n          doc! {\n            \"target.type\": \"Action\", \"target.id\": { \"$in\": ids }\n          }\n        })\n        .unwrap_or_else(|| doc! { \"target.type\": \"Action\" });\n\n      let builder_query = get_resource_ids_for_user::<Builder>(user)\n        .await?\n        .map(|ids| {\n          doc! {\n            \"target.type\": \"Builder\", \"target.id\": { \"$in\": ids }\n          }\n        })\n        .unwrap_or_else(|| doc! { \"target.type\": \"Builder\" });\n\n      let alerter_query = get_resource_ids_for_user::<Alerter>(user)\n        .await?\n        .map(|ids| {\n          doc! {\n            \"target.type\": \"Alerter\", \"target.id\": { \"$in\": ids }\n          }\n        })\n        .unwrap_or_else(|| doc! { \"target.type\": \"Alerter\" });\n\n      let resource_sync_query = get_resource_ids_for_user::<\n        ResourceSync,\n      >(user)\n      .await?\n      .map(|ids| {\n        doc! {\n          \"target.type\": \"ResourceSync\", \"target.id\": { \"$in\": ids }\n        }\n      })\n      .unwrap_or_else(|| doc! { \"target.type\": \"ResourceSync\" });\n\n      let mut query = self.query.unwrap_or_default();\n      query.extend(doc! {\n        \"$or\": [\n          server_query,\n          deployment_query,\n          stack_query,\n          build_query,\n          repo_query,\n          procedure_query,\n          action_query,\n          alerter_query,\n          builder_query,\n          resource_sync_query,\n        ]\n      });\n      query.into()\n    };\n\n    let usernames = find_collect(&db_client().users, None, None)\n      .await\n      .context(\"failed to pull users from db\")?\n      .into_iter()\n      .map(|u| (u.id, u.username))\n      .collect::<HashMap<_, _>>();\n\n    let updates = find_collect(\n      &db_client().updates,\n      query,\n      FindOptions::builder()\n        .sort(doc! { \"start_ts\": -1 })\n        .skip(self.page as u64 * UPDATES_PER_PAGE as u64)\n        .limit(UPDATES_PER_PAGE)\n        .build(),\n    )\n    .await\n    .context(\"failed to pull updates from db\")?\n    .into_iter()\n    .map(|u| {\n      let username = if User::is_service_user(&u.operator) {\n        u.operator.clone()\n      } else {\n        usernames\n          .get(&u.operator)\n          .cloned()\n          .unwrap_or(\"unknown\".to_string())\n      };\n      UpdateListItem {\n        username,\n        id: u.id,\n        operation: u.operation,\n        start_ts: u.start_ts,\n        success: u.success,\n        operator: u.operator,\n        target: u.target,\n        status: u.status,\n        version: u.version,\n        other_data: u.other_data,\n      }\n    })\n    .collect::<Vec<_>>();\n\n    let next_page = if updates.len() == UPDATES_PER_PAGE as usize {\n      Some(self.page + 1)\n    } else {\n      None\n    };\n\n    Ok(ListUpdatesResponse { updates, next_page })\n  }\n}\n\nimpl Resolve<ReadArgs> for GetUpdate {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<Update> {\n    let update = find_one_by_id(&db_client().updates, &self.id)\n      .await\n      .context(\"failed to query to db\")?\n      .context(\"no update exists with given id\")?;\n    if user.admin || core_config().transparent_mode {\n      return Ok(update);\n    }\n    match &update.target {\n      ResourceTarget::System(_) => {\n        return Err(\n          anyhow!(\"user must be admin to view system updates\").into(),\n        );\n      }\n      ResourceTarget::Server(id) => {\n        get_check_permissions::<Server>(\n          id,\n          user,\n          PermissionLevel::Read.into(),\n        )\n        .await?;\n      }\n      ResourceTarget::Deployment(id) => {\n        get_check_permissions::<Deployment>(\n          id,\n          user,\n          PermissionLevel::Read.into(),\n        )\n        .await?;\n      }\n      ResourceTarget::Build(id) => {\n        get_check_permissions::<Build>(\n          id,\n          user,\n          PermissionLevel::Read.into(),\n        )\n        .await?;\n      }\n      ResourceTarget::Repo(id) => {\n        get_check_permissions::<Repo>(\n          id,\n          user,\n          PermissionLevel::Read.into(),\n        )\n        .await?;\n      }\n      ResourceTarget::Builder(id) => {\n        get_check_permissions::<Builder>(\n          id,\n          user,\n          PermissionLevel::Read.into(),\n        )\n        .await?;\n      }\n      ResourceTarget::Alerter(id) => {\n        get_check_permissions::<Alerter>(\n          id,\n          user,\n          PermissionLevel::Read.into(),\n        )\n        .await?;\n      }\n      ResourceTarget::Procedure(id) => {\n        get_check_permissions::<Procedure>(\n          id,\n          user,\n          PermissionLevel::Read.into(),\n        )\n        .await?;\n      }\n      ResourceTarget::Action(id) => {\n        get_check_permissions::<Action>(\n          id,\n          user,\n          PermissionLevel::Read.into(),\n        )\n        .await?;\n      }\n      ResourceTarget::ResourceSync(id) => {\n        get_check_permissions::<ResourceSync>(\n          id,\n          user,\n          PermissionLevel::Read.into(),\n        )\n        .await?;\n      }\n      ResourceTarget::Stack(id) => {\n        get_check_permissions::<Stack>(\n          id,\n          user,\n          PermissionLevel::Read.into(),\n        )\n        .await?;\n      }\n    }\n    Ok(update)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/user.rs",
    "content": "use anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::find_one_by_id,\n  find::find_collect,\n  mongodb::{bson::doc, options::FindOptions},\n};\nuse komodo_client::{\n  api::read::{\n    FindUser, FindUserResponse, GetUsername, GetUsernameResponse,\n    ListApiKeys, ListApiKeysForServiceUser,\n    ListApiKeysForServiceUserResponse, ListApiKeysResponse,\n    ListUsers, ListUsersResponse,\n  },\n  entities::user::{UserConfig, admin_service_user},\n};\nuse resolver_api::Resolve;\n\nuse crate::{helpers::query::get_user, state::db_client};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetUsername {\n  async fn resolve(\n    self,\n    _: &ReadArgs,\n  ) -> serror::Result<GetUsernameResponse> {\n    if let Some(user) = admin_service_user(&self.user_id) {\n      return Ok(GetUsernameResponse {\n        username: user.username,\n        avatar: None,\n      });\n    }\n\n    let user = find_one_by_id(&db_client().users, &self.user_id)\n      .await\n      .context(\"failed at mongo query for user\")?\n      .context(\"no user found with id\")?;\n\n    let avatar = match user.config {\n      UserConfig::Github { avatar, .. } => Some(avatar),\n      UserConfig::Google { avatar, .. } => Some(avatar),\n      _ => None,\n    };\n\n    Ok(GetUsernameResponse {\n      username: user.username,\n      avatar,\n    })\n  }\n}\n\nimpl Resolve<ReadArgs> for FindUser {\n  async fn resolve(\n    self,\n    ReadArgs { user: admin }: &ReadArgs,\n  ) -> serror::Result<FindUserResponse> {\n    if !admin.admin {\n      return Err(anyhow!(\"This method is admin only.\").into());\n    }\n    Ok(get_user(&self.user).await?)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListUsers {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListUsersResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"this route is only accessable by admins\").into(),\n      );\n    }\n    let mut users = find_collect(\n      &db_client().users,\n      None,\n      FindOptions::builder().sort(doc! { \"username\": 1 }).build(),\n    )\n    .await\n    .context(\"failed to pull users from db\")?;\n    users.iter_mut().for_each(|user| user.sanitize());\n    Ok(users)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListApiKeys {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListApiKeysResponse> {\n    let api_keys = find_collect(\n      &db_client().api_keys,\n      doc! { \"user_id\": &user.id },\n      FindOptions::builder().sort(doc! { \"name\": 1 }).build(),\n    )\n    .await\n    .context(\"failed to query db for api keys\")?\n    .into_iter()\n    .map(|mut api_keys| {\n      api_keys.sanitize();\n      api_keys\n    })\n    .collect();\n    Ok(api_keys)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListApiKeysForServiceUser {\n  async fn resolve(\n    self,\n    ReadArgs { user: admin }: &ReadArgs,\n  ) -> serror::Result<ListApiKeysForServiceUserResponse> {\n    if !admin.admin {\n      return Err(anyhow!(\"This method is admin only.\").into());\n    }\n\n    let user = get_user(&self.user).await?;\n\n    let UserConfig::Service { .. } = user.config else {\n      return Err(anyhow!(\"Given user is not service user\").into());\n    };\n    let api_keys = find_collect(\n      &db_client().api_keys,\n      doc! { \"user_id\": &user.id },\n      None,\n    )\n    .await\n    .context(\"failed to query db for api keys\")?\n    .into_iter()\n    .map(|mut api_keys| {\n      api_keys.sanitize();\n      api_keys\n    })\n    .collect();\n    Ok(api_keys)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/user_group.rs",
    "content": "use std::str::FromStr;\n\nuse anyhow::Context;\nuse database::mungos::{\n  find::find_collect,\n  mongodb::{\n    bson::{Document, doc, oid::ObjectId},\n    options::FindOptions,\n  },\n};\nuse komodo_client::api::read::*;\nuse resolver_api::Resolve;\n\nuse crate::state::db_client;\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetUserGroup {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetUserGroupResponse> {\n    let mut filter = match ObjectId::from_str(&self.user_group) {\n      Ok(id) => doc! { \"_id\": id },\n      Err(_) => doc! { \"name\": &self.user_group },\n    };\n    // Don't allow non admin users to get UserGroups they aren't a part of.\n    if !user.admin {\n      // Filter for only UserGroups which contain the users id\n      filter.insert(\"users\", &user.id);\n    }\n    let res = db_client()\n      .user_groups\n      .find_one(filter)\n      .await\n      .context(\"failed to query db for user groups\")?\n      .context(\"no UserGroup found with given name or id\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListUserGroups {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListUserGroupsResponse> {\n    let mut filter = Document::new();\n    if !user.admin {\n      filter.insert(\"users\", &user.id);\n    }\n    let res = find_collect(\n      &db_client().user_groups,\n      filter,\n      FindOptions::builder().sort(doc! { \"name\": 1 }).build(),\n    )\n    .await\n    .context(\"failed to query db for UserGroups\")?;\n    Ok(res)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/read/variable.rs",
    "content": "use anyhow::Context;\nuse database::mongo_indexed::doc;\nuse database::mungos::{\n  find::find_collect, mongodb::options::FindOptions,\n};\nuse komodo_client::api::read::*;\nuse resolver_api::Resolve;\n\nuse crate::{helpers::query::get_variable, state::db_client};\n\nuse super::ReadArgs;\n\nimpl Resolve<ReadArgs> for GetVariable {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<GetVariableResponse> {\n    let mut variable = get_variable(&self.name).await?;\n    if !variable.is_secret || user.admin {\n      return Ok(variable);\n    }\n    variable.value = \"#\".repeat(variable.value.len());\n    Ok(variable)\n  }\n}\n\nimpl Resolve<ReadArgs> for ListVariables {\n  async fn resolve(\n    self,\n    ReadArgs { user }: &ReadArgs,\n  ) -> serror::Result<ListVariablesResponse> {\n    let variables = find_collect(\n      &db_client().variables,\n      None,\n      FindOptions::builder().sort(doc! { \"name\": 1 }).build(),\n    )\n    .await\n    .context(\"failed to query db for variables\")?;\n    if user.admin {\n      return Ok(variables);\n    }\n    let variables = variables\n      .into_iter()\n      .map(|mut variable| {\n        if variable.is_secret {\n          variable.value = \"#\".repeat(variable.value.len());\n        }\n        variable\n      })\n      .collect();\n    Ok(variables)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/terminal.rs",
    "content": "use anyhow::Context;\nuse axum::{Extension, Router, middleware, routing::post};\nuse komodo_client::{\n  api::terminal::*,\n  entities::{\n    deployment::Deployment, permission::PermissionLevel,\n    server::Server, stack::Stack, user::User,\n  },\n};\nuse serror::Json;\nuse uuid::Uuid;\n\nuse crate::{\n  auth::auth_request, helpers::periphery_client,\n  permission::get_check_permissions, resource::get,\n  state::stack_status_cache,\n};\n\npub fn router() -> Router {\n  Router::new()\n    .route(\"/execute\", post(execute_terminal))\n    .route(\"/execute/container\", post(execute_container_exec))\n    .route(\"/execute/deployment\", post(execute_deployment_exec))\n    .route(\"/execute/stack\", post(execute_stack_exec))\n    .layer(middleware::from_fn(auth_request))\n}\n\n// =================\n//  ExecuteTerminal\n// =================\n\nasync fn execute_terminal(\n  Extension(user): Extension<User>,\n  Json(request): Json<ExecuteTerminalBody>,\n) -> serror::Result<axum::body::Body> {\n  execute_terminal_inner(Uuid::new_v4(), request, user).await\n}\n\n#[instrument(\n  name = \"ExecuteTerminal\",\n  skip(user),\n  fields(\n    user_id = user.id,\n  )\n)]\nasync fn execute_terminal_inner(\n  req_id: Uuid,\n  ExecuteTerminalBody {\n    server,\n    terminal,\n    command,\n  }: ExecuteTerminalBody,\n  user: User,\n) -> serror::Result<axum::body::Body> {\n  info!(\"/terminal/execute request | user: {}\", user.username);\n\n  let res = async {\n    let server = get_check_permissions::<Server>(\n      &server,\n      &user,\n      PermissionLevel::Read.terminal(),\n    )\n    .await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let stream = periphery\n      .execute_terminal(terminal, command)\n      .await\n      .context(\"Failed to execute command on periphery\")?;\n\n    anyhow::Ok(stream)\n  }\n  .await;\n\n  let stream = match res {\n    Ok(stream) => stream,\n    Err(e) => {\n      warn!(\"/terminal/execute request {req_id} error: {e:#}\");\n      return Err(e.into());\n    }\n  };\n\n  Ok(axum::body::Body::from_stream(stream.into_line_stream()))\n}\n\n// ======================\n//  ExecuteContainerExec\n// ======================\n\nasync fn execute_container_exec(\n  Extension(user): Extension<User>,\n  Json(request): Json<ExecuteContainerExecBody>,\n) -> serror::Result<axum::body::Body> {\n  execute_container_exec_inner(Uuid::new_v4(), request, user).await\n}\n\n#[instrument(\n  name = \"ExecuteContainerExec\",\n  skip(user),\n  fields(\n    user_id = user.id,\n  )\n)]\nasync fn execute_container_exec_inner(\n  req_id: Uuid,\n  ExecuteContainerExecBody {\n    server,\n    container,\n    shell,\n    command,\n  }: ExecuteContainerExecBody,\n  user: User,\n) -> serror::Result<axum::body::Body> {\n  info!(\n    \"/terminal/execute/container request | user: {}\",\n    user.username\n  );\n\n  let res = async {\n    let server = get_check_permissions::<Server>(\n      &server,\n      &user,\n      PermissionLevel::Read.terminal(),\n    )\n    .await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let stream = periphery\n      .execute_container_exec(container, shell, command)\n      .await\n      .context(\n        \"Failed to execute container exec command on periphery\",\n      )?;\n\n    anyhow::Ok(stream)\n  }\n  .await;\n\n  let stream = match res {\n    Ok(stream) => stream,\n    Err(e) => {\n      warn!(\n        \"/terminal/execute/container request {req_id} error: {e:#}\"\n      );\n      return Err(e.into());\n    }\n  };\n\n  Ok(axum::body::Body::from_stream(stream.into_line_stream()))\n}\n\n// =======================\n//  ExecuteDeploymentExec\n// =======================\n\nasync fn execute_deployment_exec(\n  Extension(user): Extension<User>,\n  Json(request): Json<ExecuteDeploymentExecBody>,\n) -> serror::Result<axum::body::Body> {\n  execute_deployment_exec_inner(Uuid::new_v4(), request, user).await\n}\n\n#[instrument(\n  name = \"ExecuteDeploymentExec\",\n  skip(user),\n  fields(\n    user_id = user.id,\n  )\n)]\nasync fn execute_deployment_exec_inner(\n  req_id: Uuid,\n  ExecuteDeploymentExecBody {\n    deployment,\n    shell,\n    command,\n  }: ExecuteDeploymentExecBody,\n  user: User,\n) -> serror::Result<axum::body::Body> {\n  info!(\n    \"/terminal/execute/deployment request | user: {}\",\n    user.username\n  );\n\n  let res = async {\n    let deployment = get_check_permissions::<Deployment>(\n      &deployment,\n      &user,\n      PermissionLevel::Read.terminal(),\n    )\n    .await?;\n\n    let server = get::<Server>(&deployment.config.server_id).await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let stream = periphery\n      .execute_container_exec(deployment.name, shell, command)\n      .await\n      .context(\n        \"Failed to execute container exec command on periphery\",\n      )?;\n\n    anyhow::Ok(stream)\n  }\n  .await;\n\n  let stream = match res {\n    Ok(stream) => stream,\n    Err(e) => {\n      warn!(\n        \"/terminal/execute/deployment request {req_id} error: {e:#}\"\n      );\n      return Err(e.into());\n    }\n  };\n\n  Ok(axum::body::Body::from_stream(stream.into_line_stream()))\n}\n\n// ==================\n//  ExecuteStackExec\n// ==================\n\nasync fn execute_stack_exec(\n  Extension(user): Extension<User>,\n  Json(request): Json<ExecuteStackExecBody>,\n) -> serror::Result<axum::body::Body> {\n  execute_stack_exec_inner(Uuid::new_v4(), request, user).await\n}\n\n#[instrument(\n  name = \"ExecuteStackExec\",\n  skip(user),\n  fields(\n    user_id = user.id,\n  )\n)]\nasync fn execute_stack_exec_inner(\n  req_id: Uuid,\n  ExecuteStackExecBody {\n    stack,\n    service,\n    shell,\n    command,\n  }: ExecuteStackExecBody,\n  user: User,\n) -> serror::Result<axum::body::Body> {\n  info!(\"/terminal/execute/stack request | user: {}\", user.username);\n\n  let res = async {\n    let stack = get_check_permissions::<Stack>(\n      &stack,\n      &user,\n      PermissionLevel::Read.terminal(),\n    )\n    .await?;\n\n    let server = get::<Server>(&stack.config.server_id).await?;\n\n    let container = stack_status_cache()\n      .get(&stack.id)\n      .await\n      .context(\"could not get stack status\")?\n      .curr\n      .services\n      .iter()\n      .find(|s| s.service == service)\n      .context(\"could not find service\")?\n      .container\n      .as_ref()\n      .context(\"could not find service container\")?\n      .name\n      .clone();\n\n    let periphery = periphery_client(&server)?;\n\n    let stream = periphery\n      .execute_container_exec(container, shell, command)\n      .await\n      .context(\n        \"Failed to execute container exec command on periphery\",\n      )?;\n\n    anyhow::Ok(stream)\n  }\n  .await;\n\n  let stream = match res {\n    Ok(stream) => stream,\n    Err(e) => {\n      warn!(\"/terminal/execute/stack request {req_id} error: {e:#}\");\n      return Err(e.into());\n    }\n  };\n\n  Ok(axum::body::Body::from_stream(stream.into_line_stream()))\n}\n"
  },
  {
    "path": "bin/core/src/api/user.rs",
    "content": "use std::{collections::VecDeque, time::Instant};\n\nuse anyhow::{Context, anyhow};\nuse axum::{\n  Extension, Json, Router, extract::Path, middleware, routing::post,\n};\nuse database::mongo_indexed::doc;\nuse database::mungos::{\n  by_id::update_one_by_id, mongodb::bson::to_bson,\n};\nuse derive_variants::EnumVariants;\nuse komodo_client::{\n  api::user::*,\n  entities::{api_key::ApiKey, komodo_timestamp, user::User},\n};\nuse resolver_api::Resolve;\nuse response::Response;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse typeshare::typeshare;\nuse uuid::Uuid;\n\nuse crate::{\n  auth::auth_request,\n  helpers::{query::get_user, random_string},\n  state::db_client,\n};\n\nuse super::Variant;\n\npub struct UserArgs {\n  pub user: User,\n}\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,\n)]\n#[args(UserArgs)]\n#[response(Response)]\n#[error(serror::Error)]\n#[serde(tag = \"type\", content = \"params\")]\nenum UserRequest {\n  PushRecentlyViewed(PushRecentlyViewed),\n  SetLastSeenUpdate(SetLastSeenUpdate),\n  CreateApiKey(CreateApiKey),\n  DeleteApiKey(DeleteApiKey),\n}\n\npub fn router() -> Router {\n  Router::new()\n    .route(\"/\", post(handler))\n    .route(\"/{variant}\", post(variant_handler))\n    .layer(middleware::from_fn(auth_request))\n}\n\nasync fn variant_handler(\n  user: Extension<User>,\n  Path(Variant { variant }): Path<Variant>,\n  Json(params): Json<serde_json::Value>,\n) -> serror::Result<axum::response::Response> {\n  let req: UserRequest = serde_json::from_value(json!({\n    \"type\": variant,\n    \"params\": params,\n  }))?;\n  handler(user, Json(req)).await\n}\n\n#[instrument(name = \"UserHandler\", level = \"debug\", skip(user))]\nasync fn handler(\n  Extension(user): Extension<User>,\n  Json(request): Json<UserRequest>,\n) -> serror::Result<axum::response::Response> {\n  let timer = Instant::now();\n  let req_id = Uuid::new_v4();\n  debug!(\n    \"/user request {req_id} | user: {} ({})\",\n    user.username, user.id\n  );\n  let res = request.resolve(&UserArgs { user }).await;\n  if let Err(e) = &res {\n    warn!(\"/user request {req_id} error: {:#}\", e.error);\n  }\n  let elapsed = timer.elapsed();\n  debug!(\"/user request {req_id} | resolve time: {elapsed:?}\");\n  res.map(|res| res.0)\n}\n\nconst RECENTLY_VIEWED_MAX: usize = 10;\n\nimpl Resolve<UserArgs> for PushRecentlyViewed {\n  #[instrument(\n    name = \"PushRecentlyViewed\",\n    level = \"debug\",\n    skip(user)\n  )]\n  async fn resolve(\n    self,\n    UserArgs { user }: &UserArgs,\n  ) -> serror::Result<PushRecentlyViewedResponse> {\n    let user = get_user(&user.id).await?;\n\n    let (resource_type, id) = self.resource.extract_variant_id();\n    let update = match user.recents.get(&resource_type) {\n      Some(recents) => {\n        let mut recents = recents\n          .iter()\n          .filter(|_id| !id.eq(*_id))\n          .take(RECENTLY_VIEWED_MAX - 1)\n          .collect::<VecDeque<_>>();\n        recents.push_front(id);\n        doc! { format!(\"recents.{resource_type}\"): to_bson(&recents)? }\n      }\n      None => {\n        doc! { format!(\"recents.{resource_type}\"): [id] }\n      }\n    };\n    update_one_by_id(\n      &db_client().users,\n      &user.id,\n      database::mungos::update::Update::Set(update),\n      None,\n    )\n    .await\n    .with_context(|| {\n      format!(\"failed to update recents.{resource_type}\")\n    })?;\n\n    Ok(PushRecentlyViewedResponse {})\n  }\n}\n\nimpl Resolve<UserArgs> for SetLastSeenUpdate {\n  #[instrument(\n    name = \"SetLastSeenUpdate\",\n    level = \"debug\",\n    skip(user)\n  )]\n  async fn resolve(\n    self,\n    UserArgs { user }: &UserArgs,\n  ) -> serror::Result<SetLastSeenUpdateResponse> {\n    update_one_by_id(\n      &db_client().users,\n      &user.id,\n      database::mungos::update::Update::Set(doc! {\n        \"last_update_view\": komodo_timestamp()\n      }),\n      None,\n    )\n    .await\n    .context(\"failed to update user last_update_view\")?;\n    Ok(SetLastSeenUpdateResponse {})\n  }\n}\n\nconst SECRET_LENGTH: usize = 40;\nconst BCRYPT_COST: u32 = 10;\n\nimpl Resolve<UserArgs> for CreateApiKey {\n  #[instrument(name = \"CreateApiKey\", level = \"debug\", skip(user))]\n  async fn resolve(\n    self,\n    UserArgs { user }: &UserArgs,\n  ) -> serror::Result<CreateApiKeyResponse> {\n    let user = get_user(&user.id).await?;\n\n    let key = format!(\"K-{}\", random_string(SECRET_LENGTH));\n    let secret = format!(\"S-{}\", random_string(SECRET_LENGTH));\n    let secret_hash = bcrypt::hash(&secret, BCRYPT_COST)\n      .context(\"failed at hashing secret string\")?;\n\n    let api_key = ApiKey {\n      name: self.name,\n      key: key.clone(),\n      secret: secret_hash,\n      user_id: user.id.clone(),\n      created_at: komodo_timestamp(),\n      expires: self.expires,\n    };\n    db_client()\n      .api_keys\n      .insert_one(api_key)\n      .await\n      .context(\"failed to create api key on db\")?;\n    Ok(CreateApiKeyResponse { key, secret })\n  }\n}\n\nimpl Resolve<UserArgs> for DeleteApiKey {\n  #[instrument(name = \"DeleteApiKey\", level = \"debug\", skip(user))]\n  async fn resolve(\n    self,\n    UserArgs { user }: &UserArgs,\n  ) -> serror::Result<DeleteApiKeyResponse> {\n    let client = db_client();\n    let key = client\n      .api_keys\n      .find_one(doc! { \"key\": &self.key })\n      .await\n      .context(\"failed at db query\")?\n      .context(\"no api key with key found\")?;\n    if user.id != key.user_id {\n      return Err(anyhow!(\"api key does not belong to user\").into());\n    }\n    client\n      .api_keys\n      .delete_one(doc! { \"key\": key.key })\n      .await\n      .context(\"failed to delete api key from db\")?;\n    Ok(DeleteApiKeyResponse {})\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/action.rs",
    "content": "use komodo_client::{\n  api::write::*,\n  entities::{\n    action::Action, permission::PermissionLevel, update::Update,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{permission::get_check_permissions, resource};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateAction {\n  #[instrument(name = \"CreateAction\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Action> {\n    resource::create::<Action>(&self.name, self.config, user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for CopyAction {\n  #[instrument(name = \"CopyAction\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Action> {\n    let Action { config, .. } = get_check_permissions::<Action>(\n      &self.id,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n    resource::create::<Action>(&self.name, config.into(), user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateAction {\n  #[instrument(name = \"UpdateAction\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Action> {\n    Ok(resource::update::<Action>(&self.id, self.config, user).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameAction {\n  #[instrument(name = \"RenameAction\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    Ok(resource::rename::<Action>(&self.id, &self.name, user).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteAction {\n  #[instrument(name = \"DeleteAction\", skip(args))]\n  async fn resolve(self, args: &WriteArgs) -> serror::Result<Action> {\n    Ok(resource::delete::<Action>(&self.id, args).await?)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/alerter.rs",
    "content": "use komodo_client::{\n  api::write::*,\n  entities::{\n    alerter::Alerter, permission::PermissionLevel, update::Update,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{permission::get_check_permissions, resource};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateAlerter {\n  #[instrument(name = \"CreateAlerter\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Alerter> {\n    resource::create::<Alerter>(&self.name, self.config, user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for CopyAlerter {\n  #[instrument(name = \"CopyAlerter\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Alerter> {\n    let Alerter { config, .. } = get_check_permissions::<Alerter>(\n      &self.id,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n    resource::create::<Alerter>(&self.name, config.into(), user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteAlerter {\n  #[instrument(name = \"DeleteAlerter\", skip(args))]\n  async fn resolve(\n    self,\n    args: &WriteArgs,\n  ) -> serror::Result<Alerter> {\n    Ok(resource::delete::<Alerter>(&self.id, args).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateAlerter {\n  #[instrument(name = \"UpdateAlerter\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Alerter> {\n    Ok(\n      resource::update::<Alerter>(&self.id, self.config, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameAlerter {\n  #[instrument(name = \"RenameAlerter\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    Ok(resource::rename::<Alerter>(&self.id, &self.name, user).await?)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/build.rs",
    "content": "use std::{path::PathBuf, str::FromStr, time::Duration};\n\nuse anyhow::{Context, anyhow};\nuse database::mongo_indexed::doc;\nuse database::mungos::mongodb::bson::to_document;\nuse formatting::format_serror;\nuse komodo_client::{\n  api::write::*,\n  entities::{\n    FileContents, NoData, Operation, RepoExecutionArgs,\n    all_logs_success,\n    build::{Build, BuildInfo, PartialBuildConfig},\n    builder::{Builder, BuilderConfig},\n    config::core::CoreConfig,\n    permission::PermissionLevel,\n    repo::Repo,\n    server::ServerState,\n    update::Update,\n  },\n};\nuse octorust::types::{\n  ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,\n};\nuse periphery_client::{\n  PeripheryClient,\n  api::build::{\n    GetDockerfileContentsOnHost, WriteDockerfileContentsToHost,\n  },\n};\nuse resolver_api::Resolve;\nuse tokio::fs;\n\nuse crate::{\n  config::core_config,\n  helpers::{\n    git_token, periphery_client,\n    query::get_server_with_state,\n    update::{add_update, make_update},\n  },\n  permission::get_check_permissions,\n  resource,\n  state::{db_client, github_client},\n};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateBuild {\n  #[instrument(name = \"CreateBuild\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Build> {\n    resource::create::<Build>(&self.name, self.config, user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for CopyBuild {\n  #[instrument(name = \"CopyBuild\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Build> {\n    let Build { mut config, .. } = get_check_permissions::<Build>(\n      &self.id,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    // reset version to 0.0.0\n    config.version = Default::default();\n    resource::create::<Build>(&self.name, config.into(), user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteBuild {\n  #[instrument(name = \"DeleteBuild\", skip(args))]\n  async fn resolve(self, args: &WriteArgs) -> serror::Result<Build> {\n    Ok(resource::delete::<Build>(&self.id, args).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateBuild {\n  #[instrument(name = \"UpdateBuild\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Build> {\n    Ok(resource::update::<Build>(&self.id, self.config, user).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameBuild {\n  #[instrument(name = \"RenameBuild\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    Ok(resource::rename::<Build>(&self.id, &self.name, user).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for WriteBuildFileContents {\n  #[instrument(name = \"WriteBuildFileContents\", skip(args))]\n  async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {\n    let build = get_check_permissions::<Build>(\n      &self.build,\n      &args.user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if !build.config.files_on_host\n      && build.config.repo.is_empty()\n      && build.config.linked_repo.is_empty()\n    {\n      return Err(anyhow!(\n        \"Build is not configured to use Files on Host or Git Repo, can't write dockerfile contents\"\n      ).into());\n    }\n\n    let mut update =\n      make_update(&build, Operation::WriteDockerfile, &args.user);\n\n    update.push_simple_log(\"Dockerfile to write\", &self.contents);\n\n    if build.config.files_on_host {\n      match get_on_host_periphery(&build)\n        .await?\n        .request(WriteDockerfileContentsToHost {\n          name: build.name,\n          build_path: build.config.build_path,\n          dockerfile_path: build.config.dockerfile_path,\n          contents: self.contents,\n        })\n        .await\n        .context(\"Failed to write dockerfile contents to host\")\n      {\n        Ok(log) => {\n          update.logs.push(log);\n        }\n        Err(e) => {\n          update.push_error_log(\n            \"Write Dockerfile Contents\",\n            format_serror(&e.into()),\n          );\n        }\n      };\n\n      if !all_logs_success(&update.logs) {\n        update.finalize();\n        update.id = add_update(update.clone()).await?;\n\n        return Ok(update);\n      }\n\n      if let Err(e) =\n        (RefreshBuildCache { build: build.id }).resolve(args).await\n      {\n        update.push_error_log(\n          \"Refresh build cache\",\n          format_serror(&e.error.into()),\n        );\n      }\n\n      update.finalize();\n      update.id = add_update(update.clone()).await?;\n\n      Ok(update)\n    } else {\n      write_dockerfile_contents_git(self, args, build, update).await\n    }\n  }\n}\n\nasync fn write_dockerfile_contents_git(\n  req: WriteBuildFileContents,\n  args: &WriteArgs,\n  build: Build,\n  mut update: Update,\n) -> serror::Result<Update> {\n  let WriteBuildFileContents { build: _, contents } = req;\n\n  let mut repo_args: RepoExecutionArgs = if !build\n    .config\n    .files_on_host\n    && !build.config.linked_repo.is_empty()\n  {\n    (&crate::resource::get::<Repo>(&build.config.linked_repo).await?)\n      .into()\n  } else {\n    (&build).into()\n  };\n  let root = repo_args.unique_path(&core_config().repo_directory)?;\n  repo_args.destination = Some(root.display().to_string());\n\n  let build_path = build\n    .config\n    .build_path\n    .parse::<PathBuf>()\n    .context(\"Invalid build path\")?;\n  let dockerfile_path = build\n    .config\n    .dockerfile_path\n    .parse::<PathBuf>()\n    .context(\"Invalid dockerfile path\")?;\n\n  let full_path = root.join(&build_path).join(&dockerfile_path);\n\n  if let Some(parent) = full_path.parent() {\n    fs::create_dir_all(parent).await.with_context(|| {\n      format!(\n        \"Failed to initialize dockerfile parent directory {parent:?}\"\n      )\n    })?;\n  }\n\n  let access_token = if let Some(account) = &repo_args.account {\n    git_token(&repo_args.provider, account, |https| repo_args.https = https)\n    .await\n    .with_context(\n      || format!(\"Failed to get git token in call to db. Stopping run. | {} | {account}\", repo_args.provider),\n    )?\n  } else {\n    None\n  };\n\n  // Ensure the folder is initialized as git repo.\n  // This allows a new file to be committed on a branch that may not exist.\n  if !root.join(\".git\").exists() {\n    git::init_folder_as_repo(\n      &root,\n      &repo_args,\n      access_token.as_deref(),\n      &mut update.logs,\n    )\n    .await;\n\n    if !all_logs_success(&update.logs) {\n      update.finalize();\n      update.id = add_update(update.clone()).await?;\n\n      return Ok(update);\n    }\n  }\n\n  // Save this for later -- repo_args moved next.\n  let branch = repo_args.branch.clone();\n  // Pull latest changes to repo to ensure linear commit history\n  match git::pull_or_clone(\n    repo_args,\n    &core_config().repo_directory,\n    access_token,\n  )\n  .await\n  .context(\"Failed to pull latest changes before commit\")\n  {\n    Ok((res, _)) => update.logs.extend(res.logs),\n    Err(e) => {\n      update.push_error_log(\"Pull Repo\", format_serror(&e.into()));\n      update.finalize();\n      return Ok(update);\n    }\n  };\n\n  if !all_logs_success(&update.logs) {\n    update.finalize();\n    update.id = add_update(update.clone()).await?;\n\n    return Ok(update);\n  }\n\n  if let Err(e) =\n    fs::write(&full_path, &contents).await.with_context(|| {\n      format!(\"Failed to write dockerfile contents to {full_path:?}\")\n    })\n  {\n    update\n      .push_error_log(\"Write Dockerfile\", format_serror(&e.into()));\n  } else {\n    update.push_simple_log(\n      \"Write Dockerfile\",\n      format!(\"File written to {full_path:?}\"),\n    );\n  };\n\n  if !all_logs_success(&update.logs) {\n    update.finalize();\n    update.id = add_update(update.clone()).await?;\n\n    return Ok(update);\n  }\n\n  let commit_res = git::commit_file(\n    &format!(\"{}: Commit Dockerfile\", args.user.username),\n    &root,\n    &build_path.join(&dockerfile_path),\n    &branch,\n  )\n  .await;\n\n  update.logs.extend(commit_res.logs);\n\n  if let Err(e) = (RefreshBuildCache { build: build.name })\n    .resolve(args)\n    .await\n  {\n    update.push_error_log(\n      \"Refresh build cache\",\n      format_serror(&e.error.into()),\n    );\n  }\n\n  update.finalize();\n  update.id = add_update(update.clone()).await?;\n\n  Ok(update)\n}\n\nimpl Resolve<WriteArgs> for RefreshBuildCache {\n  #[instrument(\n    name = \"RefreshBuildCache\",\n    level = \"debug\",\n    skip(user)\n  )]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<NoData> {\n    // Even though this is a write request, this doesn't change any config. Anyone that can execute the\n    // build should be able to do this.\n    let build = get_check_permissions::<Build>(\n      &self.build,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    let repo = if !build.config.files_on_host\n      && !build.config.linked_repo.is_empty()\n    {\n      crate::resource::get::<Repo>(&build.config.linked_repo)\n        .await?\n        .into()\n    } else {\n      None\n    };\n\n    let (\n      remote_path,\n      remote_contents,\n      remote_error,\n      latest_hash,\n      latest_message,\n    ) = if build.config.files_on_host {\n      // =============\n      // FILES ON HOST\n      // =============\n      match get_on_host_dockerfile(&build).await {\n        Ok(FileContents { path, contents }) => {\n          (Some(path), Some(contents), None, None, None)\n        }\n        Err(e) => {\n          (None, None, Some(format_serror(&e.into())), None, None)\n        }\n      }\n    } else if let Some(repo) = &repo {\n      let Some(res) = get_git_remote(&build, repo.into()).await?\n      else {\n        // Nothing to do here\n        return Ok(NoData {});\n      };\n      res\n    } else if !build.config.repo.is_empty() {\n      let Some(res) = get_git_remote(&build, (&build).into()).await?\n      else {\n        // Nothing to do here\n        return Ok(NoData {});\n      };\n      res\n    } else {\n      // =============\n      // UI BASED FILE\n      // =============\n      (None, None, None, None, None)\n    };\n\n    let info = BuildInfo {\n      last_built_at: build.info.last_built_at,\n      built_hash: build.info.built_hash,\n      built_message: build.info.built_message,\n      built_contents: build.info.built_contents,\n      remote_path,\n      remote_contents,\n      remote_error,\n      latest_hash,\n      latest_message,\n    };\n\n    let info = to_document(&info)\n      .context(\"failed to serialize build info to bson\")?;\n\n    db_client()\n      .builds\n      .update_one(\n        doc! { \"name\": &build.name },\n        doc! { \"$set\": { \"info\": info } },\n      )\n      .await\n      .context(\"failed to update build info on db\")?;\n\n    Ok(NoData {})\n  }\n}\n\nasync fn get_on_host_periphery(\n  build: &Build,\n) -> anyhow::Result<PeripheryClient> {\n  if build.config.builder_id.is_empty() {\n    return Err(anyhow!(\"No builder associated with build\"));\n  }\n\n  let builder = resource::get::<Builder>(&build.config.builder_id)\n    .await\n    .context(\"Failed to get builder\")?;\n\n  match builder.config {\n    BuilderConfig::Aws(_) => {\n      Err(anyhow!(\"Files on host doesn't work with AWS builder\"))\n    }\n    BuilderConfig::Url(config) => {\n      let periphery = PeripheryClient::new(\n        config.address,\n        config.passkey,\n        Duration::from_secs(3),\n      );\n      periphery.health_check().await?;\n      Ok(periphery)\n    }\n    BuilderConfig::Server(config) => {\n      if config.server_id.is_empty() {\n        return Err(anyhow!(\n          \"Builder is type server, but has no server attached\"\n        ));\n      }\n      let (server, state) =\n        get_server_with_state(&config.server_id).await?;\n      if state != ServerState::Ok {\n        return Err(anyhow!(\n          \"Builder server is disabled or not reachable\"\n        ));\n      };\n      periphery_client(&server)\n    }\n  }\n}\n\n/// The successful case will be included as Some(remote_contents).\n/// The error case will be included as Some(remote_error)\nasync fn get_on_host_dockerfile(\n  build: &Build,\n) -> anyhow::Result<FileContents> {\n  get_on_host_periphery(build)\n    .await?\n    .request(GetDockerfileContentsOnHost {\n      name: build.name.clone(),\n      build_path: build.config.build_path.clone(),\n      dockerfile_path: build.config.dockerfile_path.clone(),\n    })\n    .await\n}\n\nasync fn get_git_remote(\n  build: &Build,\n  mut clone_args: RepoExecutionArgs,\n) -> anyhow::Result<\n  Option<(\n    Option<String>,\n    Option<String>,\n    Option<String>,\n    Option<String>,\n    Option<String>,\n  )>,\n> {\n  if clone_args.provider.is_empty() {\n    // Nothing to do here\n    return Ok(None);\n  }\n  let config = core_config();\n  let repo_path = clone_args.unique_path(&config.repo_directory)?;\n  clone_args.destination = Some(repo_path.display().to_string());\n\n  let access_token = if let Some(username) = &clone_args.account {\n    git_token(&clone_args.provider, username, |https| {\n          clone_args.https = https\n        })\n        .await\n        .with_context(\n          || format!(\"Failed to get git token in call to db. Stopping run. | {} | {username}\", clone_args.provider),\n        )?\n  } else {\n    None\n  };\n\n  let (res, _) = git::pull_or_clone(\n    clone_args,\n    &config.repo_directory,\n    access_token,\n  )\n  .await\n  .context(\"failed to clone build repo\")?;\n\n  let relative_path = PathBuf::from_str(&build.config.build_path)\n    .context(\"Invalid build path\")?\n    .join(&build.config.dockerfile_path);\n\n  let full_path = repo_path.join(&relative_path);\n  let (contents, error) =\n    match fs::read_to_string(&full_path).await.with_context(|| {\n      format!(\"Failed to read dockerfile contents at {full_path:?}\")\n    }) {\n      Ok(contents) => (Some(contents), None),\n      Err(e) => (None, Some(format_serror(&e.into()))),\n    };\n  Ok(Some((\n    Some(relative_path.display().to_string()),\n    contents,\n    error,\n    res.commit_hash,\n    res.commit_message,\n  )))\n}\n\nimpl Resolve<WriteArgs> for CreateBuildWebhook {\n  #[instrument(name = \"CreateBuildWebhook\", skip(args))]\n  async fn resolve(\n    self,\n    args: &WriteArgs,\n  ) -> serror::Result<CreateBuildWebhookResponse> {\n    let Some(github) = github_client() else {\n      return Err(\n        anyhow!(\n          \"github_webhook_app is not configured in core config toml\"\n        )\n        .into(),\n      );\n    };\n\n    let WriteArgs { user } = args;\n\n    let build = get_check_permissions::<Build>(\n      &self.build,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if build.config.repo.is_empty() {\n      return Err(\n        anyhow!(\"No repo configured, can't create webhook\").into(),\n      );\n    }\n\n    let mut split = build.config.repo.split('/');\n    let owner = split.next().context(\"Build repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Err(\n        anyhow!(\"Cannot manage repo webhooks under owner {owner}\")\n          .into(),\n      );\n    };\n\n    let repo =\n      split.next().context(\"Build repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    // First make sure the webhook isn't already created (inactive ones are ignored)\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      webhook_secret,\n      ..\n    } = core_config();\n\n    let webhook_secret = if build.config.webhook_secret.is_empty() {\n      webhook_secret\n    } else {\n      &build.config.webhook_secret\n    };\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let url = format!(\"{host}/listener/github/build/{}\", build.id);\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == url {\n        return Ok(NoData {});\n      }\n    }\n\n    // Now good to create the webhook\n    let request = ReposCreateWebhookRequest {\n      active: Some(true),\n      config: Some(ReposCreateWebhookRequestConfig {\n        url,\n        secret: webhook_secret.to_string(),\n        content_type: String::from(\"json\"),\n        insecure_ssl: None,\n        digest: Default::default(),\n        token: Default::default(),\n      }),\n      events: vec![String::from(\"push\")],\n      name: String::from(\"web\"),\n    };\n    github_repos\n      .create_webhook(owner, repo, &request)\n      .await\n      .context(\"failed to create webhook\")?;\n\n    if !build.config.webhook_enabled {\n      UpdateBuild {\n        id: build.id,\n        config: PartialBuildConfig {\n          webhook_enabled: Some(true),\n          ..Default::default()\n        },\n      }\n      .resolve(args)\n      .await\n      .map_err(|e| e.error)\n      .context(\"failed to update build to enable webhook\")?;\n    }\n\n    Ok(NoData {})\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteBuildWebhook {\n  #[instrument(name = \"DeleteBuildWebhook\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<DeleteBuildWebhookResponse> {\n    let Some(github) = github_client() else {\n      return Err(\n        anyhow!(\n          \"github_webhook_app is not configured in core config toml\"\n        )\n        .into(),\n      );\n    };\n\n    let build = get_check_permissions::<Build>(\n      &self.build,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if build.config.git_provider != \"github.com\" {\n      return Err(\n        anyhow!(\"Can only manage github.com repo webhooks\").into(),\n      );\n    }\n\n    if build.config.repo.is_empty() {\n      return Err(\n        anyhow!(\"No repo configured, can't delete webhook\").into(),\n      );\n    }\n\n    let mut split = build.config.repo.split('/');\n    let owner = split.next().context(\"Build repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Err(\n        anyhow!(\"Cannot manage repo webhooks under owner {owner}\")\n          .into(),\n      );\n    };\n\n    let repo =\n      split.next().context(\"Build repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      ..\n    } = core_config();\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let url = format!(\"{host}/listener/github/build/{}\", build.id);\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == url {\n        github_repos\n          .delete_webhook(owner, repo, webhook.id)\n          .await\n          .context(\"failed to delete webhook\")?;\n        return Ok(NoData {});\n      }\n    }\n\n    // No webhook to delete, all good\n    Ok(NoData {})\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/builder.rs",
    "content": "use komodo_client::{\n  api::write::*,\n  entities::{\n    builder::Builder, permission::PermissionLevel, update::Update,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{permission::get_check_permissions, resource};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateBuilder {\n  #[instrument(name = \"CreateBuilder\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Builder> {\n    resource::create::<Builder>(&self.name, self.config, user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for CopyBuilder {\n  #[instrument(name = \"CopyBuilder\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Builder> {\n    let Builder { config, .. } = get_check_permissions::<Builder>(\n      &self.id,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n    resource::create::<Builder>(&self.name, config.into(), user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteBuilder {\n  #[instrument(name = \"DeleteBuilder\", skip(args))]\n  async fn resolve(\n    self,\n    args: &WriteArgs,\n  ) -> serror::Result<Builder> {\n    Ok(resource::delete::<Builder>(&self.id, args).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateBuilder {\n  #[instrument(name = \"UpdateBuilder\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Builder> {\n    Ok(\n      resource::update::<Builder>(&self.id, self.config, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameBuilder {\n  #[instrument(name = \"RenameBuilder\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    Ok(resource::rename::<Builder>(&self.id, &self.name, user).await?)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/deployment.rs",
    "content": "use anyhow::{Context, anyhow};\nuse database::mungos::{by_id::update_one_by_id, mongodb::bson::doc};\nuse komodo_client::{\n  api::write::*,\n  entities::{\n    Operation,\n    deployment::{\n      Deployment, DeploymentImage, DeploymentState,\n      PartialDeploymentConfig, RestartMode,\n    },\n    docker::container::RestartPolicyNameEnum,\n    komodo_timestamp,\n    permission::PermissionLevel,\n    server::{Server, ServerState},\n    to_container_compatible_name,\n    update::Update,\n  },\n};\nuse periphery_client::api::{self, container::InspectContainer};\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::{\n    periphery_client,\n    query::get_deployment_state,\n    update::{add_update, make_update},\n  },\n  permission::get_check_permissions,\n  resource,\n  state::{action_states, db_client, server_status_cache},\n};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateDeployment {\n  #[instrument(name = \"CreateDeployment\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Deployment> {\n    resource::create::<Deployment>(&self.name, self.config, user)\n      .await\n  }\n}\n\nimpl Resolve<WriteArgs> for CopyDeployment {\n  #[instrument(name = \"CopyDeployment\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Deployment> {\n    let Deployment { config, .. } =\n      get_check_permissions::<Deployment>(\n        &self.id,\n        user,\n        PermissionLevel::Read.into(),\n      )\n      .await?;\n    resource::create::<Deployment>(&self.name, config.into(), user)\n      .await\n  }\n}\n\nimpl Resolve<WriteArgs> for CreateDeploymentFromContainer {\n  #[instrument(name = \"CreateDeploymentFromContainer\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Deployment> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Read.inspect().attach(),\n    )\n    .await?;\n    let cache = server_status_cache()\n      .get_or_insert_default(&server.id)\n      .await;\n    if cache.state != ServerState::Ok {\n      return Err(\n        anyhow!(\n          \"Cannot inspect container: server is {:?}\",\n          cache.state\n        )\n        .into(),\n      );\n    }\n    let container = periphery_client(&server)?\n      .request(InspectContainer {\n        name: self.name.clone(),\n      })\n      .await\n      .context(\"Failed to inspect container\")?;\n\n    let mut config = PartialDeploymentConfig {\n      server_id: server.id.into(),\n      ..Default::default()\n    };\n\n    if let Some(container_config) = container.config {\n      config.image = container_config\n        .image\n        .map(|image| DeploymentImage::Image { image });\n      config.command = container_config.cmd.join(\" \").into();\n      config.environment = container_config\n        .env\n        .into_iter()\n        .map(|env| format!(\"  {env}\"))\n        .collect::<Vec<_>>()\n        .join(\"\\n\")\n        .into();\n      config.labels = container_config\n        .labels\n        .into_iter()\n        .map(|(key, val)| format!(\"  {key}: {val}\"))\n        .collect::<Vec<_>>()\n        .join(\"\\n\")\n        .into();\n    }\n    if let Some(host_config) = container.host_config {\n      config.volumes = host_config\n        .binds\n        .into_iter()\n        .map(|bind| format!(\"  {bind}\"))\n        .collect::<Vec<_>>()\n        .join(\"\\n\")\n        .into();\n      config.network = host_config.network_mode;\n      config.ports = host_config\n        .port_bindings\n        .into_iter()\n        .filter_map(|(container, mut host)| {\n          let host = host.pop()?.host_port?;\n          Some(format!(\"  {host}:{}\", container.replace(\"/tcp\", \"\")))\n        })\n        .collect::<Vec<_>>()\n        .join(\"\\n\")\n        .into();\n      config.restart = host_config.restart_policy.map(|restart| {\n        match restart.name {\n          RestartPolicyNameEnum::Always => RestartMode::Always,\n          RestartPolicyNameEnum::No\n          | RestartPolicyNameEnum::Empty => RestartMode::NoRestart,\n          RestartPolicyNameEnum::UnlessStopped => {\n            RestartMode::UnlessStopped\n          }\n          RestartPolicyNameEnum::OnFailure => RestartMode::OnFailure,\n        }\n      });\n    }\n\n    resource::create::<Deployment>(&self.name, config, user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteDeployment {\n  #[instrument(name = \"DeleteDeployment\", skip(args))]\n  async fn resolve(\n    self,\n    args: &WriteArgs,\n  ) -> serror::Result<Deployment> {\n    Ok(resource::delete::<Deployment>(&self.id, args).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateDeployment {\n  #[instrument(name = \"UpdateDeployment\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Deployment> {\n    Ok(\n      resource::update::<Deployment>(&self.id, self.config, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameDeployment {\n  #[instrument(name = \"RenameDeployment\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    let deployment = get_check_permissions::<Deployment>(\n      &self.id,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    // get the action state for the deployment (or insert default).\n    let action_state = action_states()\n      .deployment\n      .get_or_insert_default(&deployment.id)\n      .await;\n\n    // Will check to ensure deployment not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.renaming = true)?;\n\n    let name = to_container_compatible_name(&self.name);\n\n    let container_state =\n      get_deployment_state(&deployment.id).await?;\n\n    if container_state == DeploymentState::Unknown {\n      return Err(\n        anyhow!(\n          \"Cannot rename Deployment when container status is unknown\"\n        )\n        .into(),\n      );\n    }\n\n    let mut update =\n      make_update(&deployment, Operation::RenameDeployment, user);\n\n    update_one_by_id(\n      &db_client().deployments,\n      &deployment.id,\n      database::mungos::update::Update::Set(\n        doc! { \"name\": &name, \"updated_at\": komodo_timestamp() },\n      ),\n      None,\n    )\n    .await\n    .context(\"Failed to update Deployment name on db\")?;\n\n    if container_state != DeploymentState::NotDeployed {\n      let server =\n        resource::get::<Server>(&deployment.config.server_id).await?;\n      let log = periphery_client(&server)?\n        .request(api::container::RenameContainer {\n          curr_name: deployment.name.clone(),\n          new_name: name.clone(),\n        })\n        .await\n        .context(\"Failed to rename container on server\")?;\n      update.logs.push(log);\n    }\n\n    update.push_simple_log(\n      \"Rename Deployment\",\n      format!(\n        \"Renamed Deployment from {} to {}\",\n        deployment.name, name\n      ),\n    );\n    update.finalize();\n    update.id = add_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/mod.rs",
    "content": "use std::time::Instant;\n\nuse anyhow::Context;\nuse axum::{\n  Extension, Router, extract::Path, middleware, routing::post,\n};\nuse derive_variants::{EnumVariants, ExtractVariant};\nuse komodo_client::{api::write::*, entities::user::User};\nuse resolver_api::Resolve;\nuse response::Response;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse serror::Json;\nuse typeshare::typeshare;\nuse uuid::Uuid;\n\nuse crate::auth::auth_request;\n\nuse super::Variant;\n\nmod action;\nmod alerter;\nmod build;\nmod builder;\nmod deployment;\nmod permissions;\nmod procedure;\nmod provider;\nmod repo;\nmod resource;\nmod server;\nmod service_user;\nmod stack;\nmod sync;\nmod tag;\nmod user;\nmod user_group;\nmod variable;\n\npub struct WriteArgs {\n  pub user: User,\n}\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,\n)]\n#[variant_derive(Debug)]\n#[args(WriteArgs)]\n#[response(Response)]\n#[error(serror::Error)]\n#[serde(tag = \"type\", content = \"params\")]\npub enum WriteRequest {\n  // ==== USER ====\n  CreateLocalUser(CreateLocalUser),\n  UpdateUserUsername(UpdateUserUsername),\n  UpdateUserPassword(UpdateUserPassword),\n  DeleteUser(DeleteUser),\n\n  // ==== SERVICE USER ====\n  CreateServiceUser(CreateServiceUser),\n  UpdateServiceUserDescription(UpdateServiceUserDescription),\n  CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),\n  DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),\n\n  // ==== USER GROUP ====\n  CreateUserGroup(CreateUserGroup),\n  RenameUserGroup(RenameUserGroup),\n  DeleteUserGroup(DeleteUserGroup),\n  AddUserToUserGroup(AddUserToUserGroup),\n  RemoveUserFromUserGroup(RemoveUserFromUserGroup),\n  SetUsersInUserGroup(SetUsersInUserGroup),\n  SetEveryoneUserGroup(SetEveryoneUserGroup),\n\n  // ==== PERMISSIONS ====\n  UpdateUserAdmin(UpdateUserAdmin),\n  UpdateUserBasePermissions(UpdateUserBasePermissions),\n  UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),\n  UpdatePermissionOnTarget(UpdatePermissionOnTarget),\n\n  // ==== RESOURCE ====\n  UpdateResourceMeta(UpdateResourceMeta),\n\n  // ==== SERVER ====\n  CreateServer(CreateServer),\n  CopyServer(CopyServer),\n  DeleteServer(DeleteServer),\n  UpdateServer(UpdateServer),\n  RenameServer(RenameServer),\n  CreateNetwork(CreateNetwork),\n  CreateTerminal(CreateTerminal),\n  DeleteTerminal(DeleteTerminal),\n  DeleteAllTerminals(DeleteAllTerminals),\n\n  // ==== STACK ====\n  CreateStack(CreateStack),\n  CopyStack(CopyStack),\n  DeleteStack(DeleteStack),\n  UpdateStack(UpdateStack),\n  RenameStack(RenameStack),\n  WriteStackFileContents(WriteStackFileContents),\n  RefreshStackCache(RefreshStackCache),\n  CreateStackWebhook(CreateStackWebhook),\n  DeleteStackWebhook(DeleteStackWebhook),\n\n  // ==== DEPLOYMENT ====\n  CreateDeployment(CreateDeployment),\n  CopyDeployment(CopyDeployment),\n  CreateDeploymentFromContainer(CreateDeploymentFromContainer),\n  DeleteDeployment(DeleteDeployment),\n  UpdateDeployment(UpdateDeployment),\n  RenameDeployment(RenameDeployment),\n\n  // ==== BUILD ====\n  CreateBuild(CreateBuild),\n  CopyBuild(CopyBuild),\n  DeleteBuild(DeleteBuild),\n  UpdateBuild(UpdateBuild),\n  RenameBuild(RenameBuild),\n  WriteBuildFileContents(WriteBuildFileContents),\n  RefreshBuildCache(RefreshBuildCache),\n  CreateBuildWebhook(CreateBuildWebhook),\n  DeleteBuildWebhook(DeleteBuildWebhook),\n\n  // ==== BUILDER ====\n  CreateBuilder(CreateBuilder),\n  CopyBuilder(CopyBuilder),\n  DeleteBuilder(DeleteBuilder),\n  UpdateBuilder(UpdateBuilder),\n  RenameBuilder(RenameBuilder),\n\n  // ==== REPO ====\n  CreateRepo(CreateRepo),\n  CopyRepo(CopyRepo),\n  DeleteRepo(DeleteRepo),\n  UpdateRepo(UpdateRepo),\n  RenameRepo(RenameRepo),\n  RefreshRepoCache(RefreshRepoCache),\n  CreateRepoWebhook(CreateRepoWebhook),\n  DeleteRepoWebhook(DeleteRepoWebhook),\n\n  // ==== ALERTER ====\n  CreateAlerter(CreateAlerter),\n  CopyAlerter(CopyAlerter),\n  DeleteAlerter(DeleteAlerter),\n  UpdateAlerter(UpdateAlerter),\n  RenameAlerter(RenameAlerter),\n\n  // ==== PROCEDURE ====\n  CreateProcedure(CreateProcedure),\n  CopyProcedure(CopyProcedure),\n  DeleteProcedure(DeleteProcedure),\n  UpdateProcedure(UpdateProcedure),\n  RenameProcedure(RenameProcedure),\n\n  // ==== ACTION ====\n  CreateAction(CreateAction),\n  CopyAction(CopyAction),\n  DeleteAction(DeleteAction),\n  UpdateAction(UpdateAction),\n  RenameAction(RenameAction),\n\n  // ==== SYNC ====\n  CreateResourceSync(CreateResourceSync),\n  CopyResourceSync(CopyResourceSync),\n  DeleteResourceSync(DeleteResourceSync),\n  UpdateResourceSync(UpdateResourceSync),\n  RenameResourceSync(RenameResourceSync),\n  WriteSyncFileContents(WriteSyncFileContents),\n  CommitSync(CommitSync),\n  RefreshResourceSyncPending(RefreshResourceSyncPending),\n  CreateSyncWebhook(CreateSyncWebhook),\n  DeleteSyncWebhook(DeleteSyncWebhook),\n\n  // ==== TAG ====\n  CreateTag(CreateTag),\n  DeleteTag(DeleteTag),\n  RenameTag(RenameTag),\n  UpdateTagColor(UpdateTagColor),\n\n  // ==== VARIABLE ====\n  CreateVariable(CreateVariable),\n  UpdateVariableValue(UpdateVariableValue),\n  UpdateVariableDescription(UpdateVariableDescription),\n  UpdateVariableIsSecret(UpdateVariableIsSecret),\n  DeleteVariable(DeleteVariable),\n\n  // ==== PROVIDERS ====\n  CreateGitProviderAccount(CreateGitProviderAccount),\n  UpdateGitProviderAccount(UpdateGitProviderAccount),\n  DeleteGitProviderAccount(DeleteGitProviderAccount),\n  CreateDockerRegistryAccount(CreateDockerRegistryAccount),\n  UpdateDockerRegistryAccount(UpdateDockerRegistryAccount),\n  DeleteDockerRegistryAccount(DeleteDockerRegistryAccount),\n}\n\npub fn router() -> Router {\n  Router::new()\n    .route(\"/\", post(handler))\n    .route(\"/{variant}\", post(variant_handler))\n    .layer(middleware::from_fn(auth_request))\n}\n\nasync fn variant_handler(\n  user: Extension<User>,\n  Path(Variant { variant }): Path<Variant>,\n  Json(params): Json<serde_json::Value>,\n) -> serror::Result<axum::response::Response> {\n  let req: WriteRequest = serde_json::from_value(json!({\n    \"type\": variant,\n    \"params\": params,\n  }))?;\n  handler(user, Json(req)).await\n}\n\nasync fn handler(\n  Extension(user): Extension<User>,\n  Json(request): Json<WriteRequest>,\n) -> serror::Result<axum::response::Response> {\n  let req_id = Uuid::new_v4();\n\n  let res = tokio::spawn(task(req_id, request, user))\n    .await\n    .context(\"failure in spawned task\");\n\n  res?\n}\n\n#[instrument(\n  name = \"WriteRequest\",\n  skip(user, request),\n  fields(\n    user_id = user.id,\n    request = format!(\"{:?}\", request.extract_variant())\n  )\n)]\nasync fn task(\n  req_id: Uuid,\n  request: WriteRequest,\n  user: User,\n) -> serror::Result<axum::response::Response> {\n  info!(\"/write request | user: {}\", user.username);\n\n  let timer = Instant::now();\n\n  let res = request.resolve(&WriteArgs { user }).await;\n\n  if let Err(e) = &res {\n    warn!(\"/write request {req_id} error: {:#}\", e.error);\n  }\n\n  let elapsed = timer.elapsed();\n  debug!(\"/write request {req_id} | resolve time: {elapsed:?}\");\n\n  res.map(|res| res.0)\n}\n"
  },
  {
    "path": "bin/core/src/api/write/permissions.rs",
    "content": "use std::str::FromStr;\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::{find_one_by_id, update_one_by_id},\n  mongodb::{\n    bson::{Document, doc, oid::ObjectId, to_bson},\n    options::UpdateOptions,\n  },\n};\nuse komodo_client::{\n  api::write::*,\n  entities::{\n    ResourceTarget, ResourceTargetVariant,\n    permission::{UserTarget, UserTargetVariant},\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{helpers::query::get_user, state::db_client};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for UpdateUserAdmin {\n  #[instrument(name = \"UpdateUserAdmin\", skip(super_admin))]\n  async fn resolve(\n    self,\n    WriteArgs { user: super_admin }: &WriteArgs,\n  ) -> serror::Result<UpdateUserAdminResponse> {\n    if !super_admin.super_admin {\n      return Err(\n        anyhow!(\"Only super admins can call this method.\").into(),\n      );\n    }\n    let user = find_one_by_id(&db_client().users, &self.user_id)\n      .await\n      .context(\"failed to query mongo for user\")?\n      .context(\"did not find user with given id\")?;\n\n    if !user.enabled {\n      return Err(\n        anyhow!(\"User is disabled. Enable user first.\").into(),\n      );\n    }\n\n    if user.super_admin {\n      return Err(anyhow!(\"Cannot update other super admins\").into());\n    }\n\n    update_one_by_id(\n      &db_client().users,\n      &self.user_id,\n      doc! { \"$set\": { \"admin\": self.admin } },\n      None,\n    )\n    .await?;\n\n    Ok(UpdateUserAdminResponse {})\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateUserBasePermissions {\n  #[instrument(name = \"UpdateUserBasePermissions\", skip(admin))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<UpdateUserBasePermissionsResponse> {\n    if !admin.admin {\n      return Err(anyhow!(\"this method is admin only\").into());\n    }\n\n    let UpdateUserBasePermissions {\n      user_id,\n      enabled,\n      create_servers,\n      create_builds,\n    } = self;\n\n    let user = find_one_by_id(&db_client().users, &user_id)\n      .await\n      .context(\"failed to query mongo for user\")?\n      .context(\"did not find user with given id\")?;\n    if user.super_admin {\n      return Err(\n        anyhow!(\n          \"Cannot use this method to update super admins permissions\"\n        )\n        .into(),\n      );\n    }\n    if user.admin && !admin.super_admin {\n      return Err(anyhow!(\n        \"Only super admins can use this method to update other admins permissions\"\n      ).into());\n    }\n    let mut update_doc = Document::new();\n    if let Some(enabled) = enabled {\n      update_doc.insert(\"enabled\", enabled);\n    }\n    if let Some(create_servers) = create_servers {\n      update_doc.insert(\"create_server_permissions\", create_servers);\n    }\n    if let Some(create_builds) = create_builds {\n      update_doc.insert(\"create_build_permissions\", create_builds);\n    }\n\n    update_one_by_id(\n      &db_client().users,\n      &user_id,\n      database::mungos::update::Update::Set(update_doc),\n      None,\n    )\n    .await?;\n\n    Ok(UpdateUserBasePermissionsResponse {})\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdatePermissionOnResourceType {\n  #[instrument(name = \"UpdatePermissionOnResourceType\", skip(admin))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<UpdatePermissionOnResourceTypeResponse> {\n    if !admin.admin {\n      return Err(anyhow!(\"this method is admin only\").into());\n    }\n\n    let Self {\n      user_target,\n      resource_type,\n      permission,\n    } = self;\n\n    // Some extra checks if user target is an actual User\n    if let UserTarget::User(user_id) = &user_target {\n      let user = get_user(user_id).await?;\n      if user.admin {\n        return Err(\n          anyhow!(\n          \"cannot use this method to update other admins permissions\"\n        )\n          .into(),\n        );\n      }\n      if !user.enabled {\n        return Err(anyhow!(\"user not enabled\").into());\n      }\n    }\n\n    let (user_target_variant, user_target_id) =\n      extract_user_target_with_validation(&user_target).await?;\n\n    let id = ObjectId::from_str(&user_target_id)\n      .context(\"id is not ObjectId\")?;\n    let filter = doc! { \"_id\": id };\n    let field = format!(\"all.{resource_type}\");\n    let set =\n      to_bson(&permission).context(\"permission is not Bson\")?;\n    let update = doc! { \"$set\": { &field: &set } };\n\n    match user_target_variant {\n      UserTargetVariant::User => {\n        db_client()\n          .users\n          .update_one(filter, update)\n          .await\n          .with_context(|| {\n            format!(\"failed to set {field}: {set} on db\")\n          })?;\n      }\n      UserTargetVariant::UserGroup => {\n        db_client()\n          .user_groups\n          .update_one(filter, update)\n          .await\n          .with_context(|| {\n            format!(\"failed to set {field}: {set} on db\")\n          })?;\n      }\n    }\n\n    Ok(UpdatePermissionOnResourceTypeResponse {})\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdatePermissionOnTarget {\n  #[instrument(name = \"UpdatePermissionOnTarget\", skip(admin))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<UpdatePermissionOnTargetResponse> {\n    if !admin.admin {\n      return Err(anyhow!(\"this method is admin only\").into());\n    }\n\n    let UpdatePermissionOnTarget {\n      user_target,\n      resource_target,\n      permission,\n    } = self;\n\n    // Some extra checks relevant if user target is an actual User\n    if let UserTarget::User(user_id) = &user_target {\n      let user = get_user(user_id).await?;\n      if !user.enabled {\n        return Err(anyhow!(\"user not enabled\").into());\n      }\n      if user.admin {\n        return Err(\n          anyhow!(\n          \"cannot use this method to update other admins permissions\"\n        )\n          .into(),\n        );\n      }\n    }\n\n    let (user_target_variant, user_target_id) =\n      extract_user_target_with_validation(&user_target).await?;\n    let (resource_variant, resource_id) =\n      extract_resource_target_with_validation(&resource_target)\n        .await?;\n\n    let (user_target_variant, resource_variant) =\n      (user_target_variant.as_ref(), resource_variant.as_ref());\n\n    let specific = to_bson(&permission.specific)\n      .context(\"permission.specific is not valid Bson\")?;\n\n    db_client()\n      .permissions\n      .update_one(\n        doc! {\n          \"user_target.type\": user_target_variant,\n          \"user_target.id\": &user_target_id,\n          \"resource_target.type\": resource_variant,\n          \"resource_target.id\": &resource_id\n        },\n        doc! {\n          \"$set\": {\n            \"user_target.type\": user_target_variant,\n            \"user_target.id\": user_target_id,\n            \"resource_target.type\": resource_variant,\n            \"resource_target.id\": resource_id,\n            \"level\": permission.level.as_ref(),\n            \"specific\": specific\n          }\n        },\n      )\n      .with_options(UpdateOptions::builder().upsert(true).build())\n      .await?;\n\n    Ok(UpdatePermissionOnTargetResponse {})\n  }\n}\n\n/// checks if inner id is actually a `name`, and replaces it with id if so.\nasync fn extract_user_target_with_validation(\n  user_target: &UserTarget,\n) -> serror::Result<(UserTargetVariant, String)> {\n  match user_target {\n    UserTarget::User(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"username\": ident },\n      };\n      let id = db_client()\n        .users\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for users\")?\n        .context(\"no matching user found\")?\n        .id;\n      Ok((UserTargetVariant::User, id))\n    }\n    UserTarget::UserGroup(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .user_groups\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for user_groups\")?\n        .context(\"no matching user_group found\")?\n        .id;\n      Ok((UserTargetVariant::UserGroup, id))\n    }\n  }\n}\n\n/// checks if inner id is actually a `name`, and replaces it with id if so.\nasync fn extract_resource_target_with_validation(\n  resource_target: &ResourceTarget,\n) -> serror::Result<(ResourceTargetVariant, String)> {\n  match resource_target {\n    ResourceTarget::System(_) => {\n      let res = resource_target.extract_variant_id();\n      Ok((res.0, res.1.clone()))\n    }\n    ResourceTarget::Build(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .builds\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for builds\")?\n        .context(\"no matching build found\")?\n        .id;\n      Ok((ResourceTargetVariant::Build, id))\n    }\n    ResourceTarget::Builder(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .builders\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for builders\")?\n        .context(\"no matching builder found\")?\n        .id;\n      Ok((ResourceTargetVariant::Builder, id))\n    }\n    ResourceTarget::Deployment(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .deployments\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for deployments\")?\n        .context(\"no matching deployment found\")?\n        .id;\n      Ok((ResourceTargetVariant::Deployment, id))\n    }\n    ResourceTarget::Server(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .servers\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for servers\")?\n        .context(\"no matching server found\")?\n        .id;\n      Ok((ResourceTargetVariant::Server, id))\n    }\n    ResourceTarget::Repo(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .repos\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for repos\")?\n        .context(\"no matching repo found\")?\n        .id;\n      Ok((ResourceTargetVariant::Repo, id))\n    }\n    ResourceTarget::Alerter(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .alerters\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for alerters\")?\n        .context(\"no matching alerter found\")?\n        .id;\n      Ok((ResourceTargetVariant::Alerter, id))\n    }\n    ResourceTarget::Procedure(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .procedures\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for procedures\")?\n        .context(\"no matching procedure found\")?\n        .id;\n      Ok((ResourceTargetVariant::Procedure, id))\n    }\n    ResourceTarget::Action(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .actions\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for actions\")?\n        .context(\"no matching action found\")?\n        .id;\n      Ok((ResourceTargetVariant::Action, id))\n    }\n    ResourceTarget::ResourceSync(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .resource_syncs\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for resource syncs\")?\n        .context(\"no matching resource sync found\")?\n        .id;\n      Ok((ResourceTargetVariant::ResourceSync, id))\n    }\n    ResourceTarget::Stack(ident) => {\n      let filter = match ObjectId::from_str(ident) {\n        Ok(id) => doc! { \"_id\": id },\n        Err(_) => doc! { \"name\": ident },\n      };\n      let id = db_client()\n        .stacks\n        .find_one(filter)\n        .await\n        .context(\"failed to query db for stacks\")?\n        .context(\"no matching stack found\")?\n        .id;\n      Ok((ResourceTargetVariant::Stack, id))\n    }\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/procedure.rs",
    "content": "use komodo_client::{\n  api::write::*,\n  entities::{\n    permission::PermissionLevel, procedure::Procedure, update::Update,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{permission::get_check_permissions, resource};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateProcedure {\n  #[instrument(name = \"CreateProcedure\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<CreateProcedureResponse> {\n    resource::create::<Procedure>(&self.name, self.config, user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for CopyProcedure {\n  #[instrument(name = \"CopyProcedure\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<CopyProcedureResponse> {\n    let Procedure { config, .. } =\n      get_check_permissions::<Procedure>(\n        &self.id,\n        user,\n        PermissionLevel::Write.into(),\n      )\n      .await?;\n    resource::create::<Procedure>(&self.name, config.into(), user)\n      .await\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateProcedure {\n  #[instrument(name = \"UpdateProcedure\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<UpdateProcedureResponse> {\n    Ok(\n      resource::update::<Procedure>(&self.id, self.config, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameProcedure {\n  #[instrument(name = \"RenameProcedure\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    Ok(\n      resource::rename::<Procedure>(&self.id, &self.name, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteProcedure {\n  #[instrument(name = \"DeleteProcedure\", skip(args))]\n  async fn resolve(\n    self,\n    args: &WriteArgs,\n  ) -> serror::Result<DeleteProcedureResponse> {\n    Ok(resource::delete::<Procedure>(&self.id, args).await?)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/provider.rs",
    "content": "use anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::{delete_one_by_id, find_one_by_id, update_one_by_id},\n  mongodb::bson::{doc, to_document},\n};\nuse komodo_client::{\n  api::write::*,\n  entities::{\n    Operation, ResourceTarget,\n    provider::{DockerRegistryAccount, GitProviderAccount},\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::update::{add_update, make_update},\n  state::db_client,\n};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateGitProviderAccount {\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<CreateGitProviderAccountResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"only admins can create git provider accounts\")\n          .into(),\n      );\n    }\n\n    let mut account: GitProviderAccount = self.account.into();\n\n    if account.domain.is_empty() {\n      return Err(anyhow!(\"domain cannot be empty string.\").into());\n    }\n\n    if account.username.is_empty() {\n      return Err(anyhow!(\"username cannot be empty string.\").into());\n    }\n\n    let mut update = make_update(\n      ResourceTarget::system(),\n      Operation::CreateGitProviderAccount,\n      user,\n    );\n\n    account.id = db_client()\n      .git_accounts\n      .insert_one(&account)\n      .await\n      .context(\"failed to create git provider account on db\")?\n      .inserted_id\n      .as_object_id()\n      .context(\"inserted id is not ObjectId\")?\n      .to_string();\n\n    update.push_simple_log(\n      \"create git provider account\",\n      format!(\n        \"Created git provider account for {} with username {}\",\n        account.domain, account.username\n      ),\n    );\n\n    update.finalize();\n\n    add_update(update)\n      .await\n      .inspect_err(|e| {\n        error!(\"failed to add update for create git provider account | {e:#}\")\n      })\n      .ok();\n\n    Ok(account)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateGitProviderAccount {\n  async fn resolve(\n    mut self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<UpdateGitProviderAccountResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"only admins can update git provider accounts\")\n          .into(),\n      );\n    }\n\n    if let Some(domain) = &self.account.domain\n      && domain.is_empty()\n    {\n      return Err(\n        anyhow!(\"cannot update git provider with empty domain\")\n          .into(),\n      );\n    }\n\n    if let Some(username) = &self.account.username\n      && username.is_empty()\n    {\n      return Err(\n        anyhow!(\"cannot update git provider with empty username\")\n          .into(),\n      );\n    }\n\n    // Ensure update does not change id\n    self.account.id = None;\n\n    let mut update = make_update(\n      ResourceTarget::system(),\n      Operation::UpdateGitProviderAccount,\n      user,\n    );\n\n    let account = to_document(&self.account).context(\n      \"failed to serialize partial git provider account to bson\",\n    )?;\n    let db = db_client();\n    update_one_by_id(\n      &db.git_accounts,\n      &self.id,\n      doc! { \"$set\": account },\n      None,\n    )\n    .await\n    .context(\"failed to update git provider account on db\")?;\n\n    let Some(account) = find_one_by_id(&db.git_accounts, &self.id)\n      .await\n      .context(\"failed to query db for git accounts\")?\n    else {\n      return Err(anyhow!(\"no account found with given id\").into());\n    };\n\n    update.push_simple_log(\n      \"update git provider account\",\n      format!(\n        \"Updated git provider account for {} with username {}\",\n        account.domain, account.username\n      ),\n    );\n\n    update.finalize();\n\n    add_update(update)\n      .await\n      .inspect_err(|e| {\n        error!(\"failed to add update for update git provider account | {e:#}\")\n      })\n      .ok();\n\n    Ok(account)\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteGitProviderAccount {\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<DeleteGitProviderAccountResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"only admins can delete git provider accounts\")\n          .into(),\n      );\n    }\n\n    let mut update = make_update(\n      ResourceTarget::system(),\n      Operation::UpdateGitProviderAccount,\n      user,\n    );\n\n    let db = db_client();\n    let Some(account) = find_one_by_id(&db.git_accounts, &self.id)\n      .await\n      .context(\"failed to query db for git accounts\")?\n    else {\n      return Err(anyhow!(\"no account found with given id\").into());\n    };\n    delete_one_by_id(&db.git_accounts, &self.id, None)\n      .await\n      .context(\"failed to delete git account on db\")?;\n\n    update.push_simple_log(\n      \"delete git provider account\",\n      format!(\n        \"Deleted git provider account for {} with username {}\",\n        account.domain, account.username\n      ),\n    );\n\n    update.finalize();\n\n    add_update(update)\n      .await\n      .inspect_err(|e| {\n        error!(\"failed to add update for delete git provider account | {e:#}\")\n      })\n      .ok();\n\n    Ok(account)\n  }\n}\n\nimpl Resolve<WriteArgs> for CreateDockerRegistryAccount {\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<CreateDockerRegistryAccountResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\n          \"only admins can create docker registry account accounts\"\n        )\n        .into(),\n      );\n    }\n\n    let mut account: DockerRegistryAccount = self.account.into();\n\n    if account.domain.is_empty() {\n      return Err(anyhow!(\"domain cannot be empty string.\").into());\n    }\n\n    if account.username.is_empty() {\n      return Err(anyhow!(\"username cannot be empty string.\").into());\n    }\n\n    let mut update = make_update(\n      ResourceTarget::system(),\n      Operation::CreateDockerRegistryAccount,\n      user,\n    );\n\n    account.id = db_client()\n      .registry_accounts\n      .insert_one(&account)\n      .await\n      .context(\n        \"failed to create docker registry account account on db\",\n      )?\n      .inserted_id\n      .as_object_id()\n      .context(\"inserted id is not ObjectId\")?\n      .to_string();\n\n    update.push_simple_log(\n      \"create docker registry account\",\n      format!(\n        \"Created docker registry account account for {} with username {}\",\n        account.domain, account.username\n      ),\n    );\n\n    update.finalize();\n\n    add_update(update)\n      .await\n      .inspect_err(|e| {\n        error!(\"failed to add update for create docker registry account | {e:#}\")\n      })\n      .ok();\n\n    Ok(account)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateDockerRegistryAccount {\n  async fn resolve(\n    mut self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<UpdateDockerRegistryAccountResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"only admins can update docker registry accounts\")\n          .into(),\n      );\n    }\n\n    if let Some(domain) = &self.account.domain\n      && domain.is_empty()\n    {\n      return Err(\n        anyhow!(\n          \"cannot update docker registry account with empty domain\"\n        )\n        .into(),\n      );\n    }\n\n    if let Some(username) = &self.account.username\n      && username.is_empty()\n    {\n      return Err(\n        anyhow!(\n          \"cannot update docker registry account with empty username\"\n        )\n        .into(),\n      );\n    }\n\n    self.account.id = None;\n\n    let mut update = make_update(\n      ResourceTarget::system(),\n      Operation::UpdateDockerRegistryAccount,\n      user,\n    );\n\n    let account = to_document(&self.account).context(\n      \"failed to serialize partial docker registry account account to bson\",\n    )?;\n\n    let db = db_client();\n    update_one_by_id(\n      &db.registry_accounts,\n      &self.id,\n      doc! { \"$set\": account },\n      None,\n    )\n    .await\n    .context(\n      \"failed to update docker registry account account on db\",\n    )?;\n\n    let Some(account) =\n      find_one_by_id(&db.registry_accounts, &self.id)\n        .await\n        .context(\"failed to query db for registry accounts\")?\n    else {\n      return Err(anyhow!(\"no account found with given id\").into());\n    };\n\n    update.push_simple_log(\n      \"update docker registry account\",\n      format!(\n        \"Updated docker registry account account for {} with username {}\",\n        account.domain, account.username\n      ),\n    );\n\n    update.finalize();\n\n    add_update(update)\n      .await\n      .inspect_err(|e| {\n        error!(\"failed to add update for update docker registry account | {e:#}\")\n      })\n      .ok();\n\n    Ok(account)\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteDockerRegistryAccount {\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<DeleteDockerRegistryAccountResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"only admins can delete docker registry accounts\")\n          .into(),\n      );\n    }\n\n    let mut update = make_update(\n      ResourceTarget::system(),\n      Operation::UpdateDockerRegistryAccount,\n      user,\n    );\n\n    let db = db_client();\n    let Some(account) =\n      find_one_by_id(&db.registry_accounts, &self.id)\n        .await\n        .context(\"failed to query db for git accounts\")?\n    else {\n      return Err(anyhow!(\"no account found with given id\").into());\n    };\n    delete_one_by_id(&db.registry_accounts, &self.id, None)\n      .await\n      .context(\"failed to delete registry account on db\")?;\n\n    update.push_simple_log(\n      \"delete registry account\",\n      format!(\n        \"Deleted registry account for {} with username {}\",\n        account.domain, account.username\n      ),\n    );\n\n    update.finalize();\n\n    add_update(update)\n      .await\n      .inspect_err(|e| {\n        error!(\"failed to add update for delete docker registry account | {e:#}\")\n      })\n      .ok();\n\n    Ok(account)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/repo.rs",
    "content": "use anyhow::{Context, anyhow};\nuse database::mongo_indexed::doc;\nuse database::mungos::{\n  by_id::update_one_by_id, mongodb::bson::to_document,\n};\nuse formatting::format_serror;\nuse komodo_client::{\n  api::write::*,\n  entities::{\n    NoData, Operation, RepoExecutionArgs,\n    config::core::CoreConfig,\n    komodo_timestamp,\n    permission::PermissionLevel,\n    repo::{PartialRepoConfig, Repo, RepoInfo},\n    server::Server,\n    to_path_compatible_name,\n    update::{Log, Update},\n  },\n};\nuse octorust::types::{\n  ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,\n};\nuse periphery_client::api;\nuse resolver_api::Resolve;\n\nuse crate::{\n  config::core_config,\n  helpers::{\n    git_token, periphery_client,\n    update::{add_update, make_update},\n  },\n  permission::get_check_permissions,\n  resource,\n  state::{action_states, db_client, github_client},\n};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateRepo {\n  #[instrument(name = \"CreateRepo\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Repo> {\n    resource::create::<Repo>(&self.name, self.config, user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for CopyRepo {\n  #[instrument(name = \"CopyRepo\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Repo> {\n    let Repo { config, .. } = get_check_permissions::<Repo>(\n      &self.id,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n    resource::create::<Repo>(&self.name, config.into(), user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteRepo {\n  #[instrument(name = \"DeleteRepo\", skip(args))]\n  async fn resolve(self, args: &WriteArgs) -> serror::Result<Repo> {\n    Ok(resource::delete::<Repo>(&self.id, args).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateRepo {\n  #[instrument(name = \"UpdateRepo\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Repo> {\n    Ok(resource::update::<Repo>(&self.id, self.config, user).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameRepo {\n  #[instrument(name = \"RenameRepo\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    let repo = get_check_permissions::<Repo>(\n      &self.id,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if repo.config.server_id.is_empty()\n      || !repo.config.path.is_empty()\n    {\n      return Ok(\n        resource::rename::<Repo>(&repo.id, &self.name, user).await?,\n      );\n    }\n\n    // get the action state for the repo (or insert default).\n    let action_state =\n      action_states().repo.get_or_insert_default(&repo.id).await;\n\n    // Will check to ensure repo not already busy before updating, and return Err if so.\n    // The returned guard will set the action state back to default when dropped.\n    let _action_guard =\n      action_state.update(|state| state.renaming = true)?;\n\n    let name = to_path_compatible_name(&self.name);\n\n    let mut update = make_update(&repo, Operation::RenameRepo, user);\n\n    update_one_by_id(\n      &db_client().repos,\n      &repo.id,\n      database::mungos::update::Update::Set(\n        doc! { \"name\": &name, \"updated_at\": komodo_timestamp() },\n      ),\n      None,\n    )\n    .await\n    .context(\"Failed to update Repo name on db\")?;\n\n    let server =\n      resource::get::<Server>(&repo.config.server_id).await?;\n\n    let log = match periphery_client(&server)?\n      .request(api::git::RenameRepo {\n        curr_name: to_path_compatible_name(&repo.name),\n        new_name: name.clone(),\n      })\n      .await\n      .context(\"Failed to rename Repo directory on Server\")\n    {\n      Ok(log) => log,\n      Err(e) => Log::error(\n        \"Rename Repo directory failure\",\n        format_serror(&e.into()),\n      ),\n    };\n\n    update.logs.push(log);\n\n    update.push_simple_log(\n      \"Rename Repo\",\n      format!(\"Renamed Repo from {} to {}\", repo.name, name),\n    );\n    update.finalize();\n    update.id = add_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<WriteArgs> for RefreshRepoCache {\n  #[instrument(\n    name = \"RefreshRepoCache\",\n    level = \"debug\",\n    skip(user)\n  )]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<NoData> {\n    // Even though this is a write request, this doesn't change any config. Anyone that can execute the\n    // repo should be able to do this.\n    let repo = get_check_permissions::<Repo>(\n      &self.repo,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    if repo.config.git_provider.is_empty()\n      || repo.config.repo.is_empty()\n    {\n      // Nothing to do\n      return Ok(NoData {});\n    }\n\n    let mut clone_args: RepoExecutionArgs = (&repo).into();\n    let repo_path =\n      clone_args.unique_path(&core_config().repo_directory)?;\n    clone_args.destination = Some(repo_path.display().to_string());\n\n    let access_token = if let Some(username) = &clone_args.account {\n      git_token(&clone_args.provider, username, |https| {\n          clone_args.https = https\n        })\n        .await\n        .with_context(\n          || format!(\"Failed to get git token in call to db. Stopping run. | {} | {username}\", clone_args.provider),\n        )?\n    } else {\n      None\n    };\n\n    let (res, _) = git::pull_or_clone(\n      clone_args,\n      &core_config().repo_directory,\n      access_token,\n    )\n    .await\n    .with_context(|| {\n      format!(\"Failed to update repo at {repo_path:?}\")\n    })?;\n\n    let info = RepoInfo {\n      last_pulled_at: repo.info.last_pulled_at,\n      last_built_at: repo.info.last_built_at,\n      built_hash: repo.info.built_hash,\n      built_message: repo.info.built_message,\n      latest_hash: res.commit_hash,\n      latest_message: res.commit_message,\n    };\n\n    let info = to_document(&info)\n      .context(\"failed to serialize repo info to bson\")?;\n\n    db_client()\n      .repos\n      .update_one(\n        doc! { \"name\": &repo.name },\n        doc! { \"$set\": { \"info\": info } },\n      )\n      .await\n      .context(\"failed to update repo info on db\")?;\n\n    Ok(NoData {})\n  }\n}\n\nimpl Resolve<WriteArgs> for CreateRepoWebhook {\n  #[instrument(name = \"CreateRepoWebhook\", skip(args))]\n  async fn resolve(\n    self,\n    args: &WriteArgs,\n  ) -> serror::Result<CreateRepoWebhookResponse> {\n    let Some(github) = github_client() else {\n      return Err(\n        anyhow!(\n          \"github_webhook_app is not configured in core config toml\"\n        )\n        .into(),\n      );\n    };\n\n    let repo = get_check_permissions::<Repo>(\n      &self.repo,\n      &args.user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if repo.config.repo.is_empty() {\n      return Err(\n        anyhow!(\"No repo configured, can't create webhook\").into(),\n      );\n    }\n\n    let mut split = repo.config.repo.split('/');\n    let owner = split.next().context(\"Repo repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Err(\n        anyhow!(\"Cannot manage repo webhooks under owner {owner}\")\n          .into(),\n      );\n    };\n\n    let repo_name =\n      split.next().context(\"Repo repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    // First make sure the webhook isn't already created (inactive ones are ignored)\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo_name)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      webhook_secret,\n      ..\n    } = core_config();\n\n    let webhook_secret = if repo.config.webhook_secret.is_empty() {\n      webhook_secret\n    } else {\n      &repo.config.webhook_secret\n    };\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let url = match self.action {\n      RepoWebhookAction::Clone => {\n        format!(\"{host}/listener/github/repo/{}/clone\", repo.id)\n      }\n      RepoWebhookAction::Pull => {\n        format!(\"{host}/listener/github/repo/{}/pull\", repo.id)\n      }\n      RepoWebhookAction::Build => {\n        format!(\"{host}/listener/github/repo/{}/build\", repo.id)\n      }\n    };\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == url {\n        return Ok(NoData {});\n      }\n    }\n\n    // Now good to create the webhook\n    let request = ReposCreateWebhookRequest {\n      active: Some(true),\n      config: Some(ReposCreateWebhookRequestConfig {\n        url,\n        secret: webhook_secret.to_string(),\n        content_type: String::from(\"json\"),\n        insecure_ssl: None,\n        digest: Default::default(),\n        token: Default::default(),\n      }),\n      events: vec![String::from(\"push\")],\n      name: String::from(\"web\"),\n    };\n    github_repos\n      .create_webhook(owner, repo_name, &request)\n      .await\n      .context(\"failed to create webhook\")?;\n\n    if !repo.config.webhook_enabled {\n      UpdateRepo {\n        id: repo.id,\n        config: PartialRepoConfig {\n          webhook_enabled: Some(true),\n          ..Default::default()\n        },\n      }\n      .resolve(args)\n      .await\n      .map_err(|e| e.error)\n      .context(\"failed to update repo to enable webhook\")?;\n    }\n\n    Ok(NoData {})\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteRepoWebhook {\n  #[instrument(name = \"DeleteRepoWebhook\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<DeleteRepoWebhookResponse> {\n    let Some(github) = github_client() else {\n      return Err(\n        anyhow!(\n          \"github_webhook_app is not configured in core config toml\"\n        )\n        .into(),\n      );\n    };\n\n    let repo = get_check_permissions::<Repo>(\n      &self.repo,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if repo.config.git_provider != \"github.com\" {\n      return Err(\n        anyhow!(\"Can only manage github.com repo webhooks\").into(),\n      );\n    }\n\n    if repo.config.repo.is_empty() {\n      return Err(\n        anyhow!(\"No repo configured, can't create webhook\").into(),\n      );\n    }\n\n    let mut split = repo.config.repo.split('/');\n    let owner = split.next().context(\"Repo repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Err(\n        anyhow!(\"Cannot manage repo webhooks under owner {owner}\")\n          .into(),\n      );\n    };\n\n    let repo_name =\n      split.next().context(\"Repo repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    // First make sure the webhook isn't already created (inactive ones are ignored)\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo_name)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      ..\n    } = core_config();\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let url = match self.action {\n      RepoWebhookAction::Clone => {\n        format!(\"{host}/listener/github/repo/{}/clone\", repo.id)\n      }\n      RepoWebhookAction::Pull => {\n        format!(\"{host}/listener/github/repo/{}/pull\", repo.id)\n      }\n      RepoWebhookAction::Build => {\n        format!(\"{host}/listener/github/repo/{}/build\", repo.id)\n      }\n    };\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == url {\n        github_repos\n          .delete_webhook(owner, repo_name, webhook.id)\n          .await\n          .context(\"failed to delete webhook\")?;\n        return Ok(NoData {});\n      }\n    }\n\n    // No webhook to delete, all good\n    Ok(NoData {})\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/resource.rs",
    "content": "use anyhow::anyhow;\nuse komodo_client::{\n  api::write::{UpdateResourceMeta, UpdateResourceMetaResponse},\n  entities::{\n    ResourceTarget, action::Action, alerter::Alerter, build::Build,\n    builder::Builder, deployment::Deployment, procedure::Procedure,\n    repo::Repo, server::Server, stack::Stack, sync::ResourceSync,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::resource::{self, ResourceMetaUpdate};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for UpdateResourceMeta {\n  #[instrument(name = \"UpdateResourceMeta\", skip(args))]\n  async fn resolve(\n    self,\n    args: &WriteArgs,\n  ) -> serror::Result<UpdateResourceMetaResponse> {\n    let meta = ResourceMetaUpdate {\n      description: self.description,\n      template: self.template,\n      tags: self.tags,\n    };\n    match self.target {\n      ResourceTarget::System(_) => {\n        return Err(\n          anyhow!(\"cannot update meta of System resource target\")\n            .into(),\n        );\n      }\n      ResourceTarget::Server(id) => {\n        resource::update_meta::<Server>(&id, meta, args).await?;\n      }\n      ResourceTarget::Deployment(id) => {\n        resource::update_meta::<Deployment>(&id, meta, args).await?;\n      }\n      ResourceTarget::Build(id) => {\n        resource::update_meta::<Build>(&id, meta, args).await?;\n      }\n      ResourceTarget::Repo(id) => {\n        resource::update_meta::<Repo>(&id, meta, args).await?;\n      }\n      ResourceTarget::Builder(id) => {\n        resource::update_meta::<Builder>(&id, meta, args).await?;\n      }\n      ResourceTarget::Alerter(id) => {\n        resource::update_meta::<Alerter>(&id, meta, args).await?;\n      }\n      ResourceTarget::Procedure(id) => {\n        resource::update_meta::<Procedure>(&id, meta, args).await?;\n      }\n      ResourceTarget::Action(id) => {\n        resource::update_meta::<Action>(&id, meta, args).await?;\n      }\n      ResourceTarget::ResourceSync(id) => {\n        resource::update_meta::<ResourceSync>(&id, meta, args)\n          .await?;\n      }\n      ResourceTarget::Stack(id) => {\n        resource::update_meta::<Stack>(&id, meta, args).await?;\n      }\n    }\n    Ok(UpdateResourceMetaResponse {})\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/server.rs",
    "content": "use anyhow::Context;\nuse formatting::format_serror;\nuse komodo_client::{\n  api::write::*,\n  entities::{\n    NoData, Operation,\n    permission::PermissionLevel,\n    server::Server,\n    to_docker_compatible_name,\n    update::{Update, UpdateStatus},\n  },\n};\nuse periphery_client::api;\nuse resolver_api::Resolve;\n\nuse crate::{\n  helpers::{\n    periphery_client,\n    update::{add_update, make_update, update_update},\n  },\n  permission::get_check_permissions,\n  resource,\n};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateServer {\n  #[instrument(name = \"CreateServer\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Server> {\n    resource::create::<Server>(&self.name, self.config, user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for CopyServer {\n  #[instrument(name = \"CopyServer\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Server> {\n    let Server { config, .. } = get_check_permissions::<Server>(\n      &self.id,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n\n    resource::create::<Server>(&self.name, config.into(), user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteServer {\n  #[instrument(name = \"DeleteServer\", skip(args))]\n  async fn resolve(self, args: &WriteArgs) -> serror::Result<Server> {\n    Ok(resource::delete::<Server>(&self.id, args).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateServer {\n  #[instrument(name = \"UpdateServer\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Server> {\n    Ok(resource::update::<Server>(&self.id, self.config, user).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameServer {\n  #[instrument(name = \"RenameServer\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    Ok(resource::rename::<Server>(&self.id, &self.name, user).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for CreateNetwork {\n  #[instrument(name = \"CreateNetwork\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    let periphery = periphery_client(&server)?;\n\n    let mut update =\n      make_update(&server, Operation::CreateNetwork, user);\n    update.status = UpdateStatus::InProgress;\n    update.id = add_update(update.clone()).await?;\n\n    match periphery\n      .request(api::network::CreateNetwork {\n        name: to_docker_compatible_name(&self.name),\n        driver: None,\n      })\n      .await\n    {\n      Ok(log) => update.logs.push(log),\n      Err(e) => update.push_error_log(\n        \"create network\",\n        format_serror(&e.context(\"failed to create network\").into()),\n      ),\n    };\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nimpl Resolve<WriteArgs> for CreateTerminal {\n  #[instrument(name = \"CreateTerminal\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<NoData> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Write.terminal(),\n    )\n    .await?;\n\n    let periphery = periphery_client(&server)?;\n\n    periphery\n      .request(api::terminal::CreateTerminal {\n        name: self.name,\n        command: self.command,\n        recreate: self.recreate,\n      })\n      .await\n      .context(\"Failed to create terminal on periphery\")?;\n\n    Ok(NoData {})\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteTerminal {\n  #[instrument(name = \"DeleteTerminal\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<NoData> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Write.terminal(),\n    )\n    .await?;\n\n    let periphery = periphery_client(&server)?;\n\n    periphery\n      .request(api::terminal::DeleteTerminal {\n        terminal: self.terminal,\n      })\n      .await\n      .context(\"Failed to delete terminal on periphery\")?;\n\n    Ok(NoData {})\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteAllTerminals {\n  #[instrument(name = \"DeleteAllTerminals\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<NoData> {\n    let server = get_check_permissions::<Server>(\n      &self.server,\n      user,\n      PermissionLevel::Write.terminal(),\n    )\n    .await?;\n\n    let periphery = periphery_client(&server)?;\n\n    periphery\n      .request(api::terminal::DeleteAllTerminals {})\n      .await\n      .context(\"Failed to delete all terminals on periphery\")?;\n\n    Ok(NoData {})\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/service_user.rs",
    "content": "use std::str::FromStr;\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::find_one_by_id,\n  mongodb::bson::{doc, oid::ObjectId},\n};\nuse komodo_client::{\n  api::{user::CreateApiKey, write::*},\n  entities::{\n    komodo_timestamp,\n    user::{User, UserConfig},\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{api::user::UserArgs, state::db_client};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateServiceUser {\n  #[instrument(name = \"CreateServiceUser\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<CreateServiceUserResponse> {\n    if !user.admin {\n      return Err(anyhow!(\"user not admin\").into());\n    }\n    if ObjectId::from_str(&self.username).is_ok() {\n      return Err(\n        anyhow!(\"username cannot be valid ObjectId\").into(),\n      );\n    }\n    let config = UserConfig::Service {\n      description: self.description,\n    };\n    let mut user = User {\n      id: Default::default(),\n      username: self.username,\n      config,\n      enabled: true,\n      admin: false,\n      super_admin: false,\n      create_server_permissions: false,\n      create_build_permissions: false,\n      last_update_view: 0,\n      recents: Default::default(),\n      all: Default::default(),\n      updated_at: komodo_timestamp(),\n    };\n    user.id = db_client()\n      .users\n      .insert_one(&user)\n      .await\n      .context(\"failed to create service user on db\")?\n      .inserted_id\n      .as_object_id()\n      .context(\"inserted id is not object id\")?\n      .to_string();\n    Ok(user)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateServiceUserDescription {\n  #[instrument(name = \"UpdateServiceUserDescription\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<UpdateServiceUserDescriptionResponse> {\n    if !user.admin {\n      return Err(anyhow!(\"user not admin\").into());\n    }\n    let db = db_client();\n    let service_user = db\n      .users\n      .find_one(doc! { \"username\": &self.username })\n      .await\n      .context(\"failed to query db for user\")?\n      .context(\"no user with given username\")?;\n    let UserConfig::Service { .. } = &service_user.config else {\n      return Err(anyhow!(\"user is not service user\").into());\n    };\n    db.users\n      .update_one(\n        doc! { \"username\": &self.username },\n        doc! { \"$set\": { \"config.data.description\": self.description } },\n      )\n      .await\n      .context(\"failed to update user on db\")?;\n    let res = db\n      .users\n      .find_one(doc! { \"username\": &self.username })\n      .await\n      .context(\"failed to query db for user\")?\n      .context(\"user with username not found\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<WriteArgs> for CreateApiKeyForServiceUser {\n  #[instrument(name = \"CreateApiKeyForServiceUser\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<CreateApiKeyForServiceUserResponse> {\n    if !user.admin {\n      return Err(anyhow!(\"user not admin\").into());\n    }\n    let service_user =\n      find_one_by_id(&db_client().users, &self.user_id)\n        .await\n        .context(\"failed to query db for user\")?\n        .context(\"no user found with id\")?;\n    let UserConfig::Service { .. } = &service_user.config else {\n      return Err(anyhow!(\"user is not service user\").into());\n    };\n    CreateApiKey {\n      name: self.name,\n      expires: self.expires,\n    }\n    .resolve(&UserArgs { user: service_user })\n    .await\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteApiKeyForServiceUser {\n  #[instrument(name = \"DeleteApiKeyForServiceUser\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<DeleteApiKeyForServiceUserResponse> {\n    if !user.admin {\n      return Err(anyhow!(\"user not admin\").into());\n    }\n    let db = db_client();\n    let api_key = db\n      .api_keys\n      .find_one(doc! { \"key\": &self.key })\n      .await\n      .context(\"failed to query db for api key\")?\n      .context(\"did not find matching api key\")?;\n    let service_user =\n      find_one_by_id(&db_client().users, &api_key.user_id)\n        .await\n        .context(\"failed to query db for user\")?\n        .context(\"no user found with id\")?;\n    let UserConfig::Service { .. } = &service_user.config else {\n      return Err(anyhow!(\"user is not service user\").into());\n    };\n    db.api_keys\n      .delete_one(doc! { \"key\": self.key })\n      .await\n      .context(\"failed to delete api key on db\")?;\n    Ok(DeleteApiKeyForServiceUserResponse {})\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/stack.rs",
    "content": "use std::path::PathBuf;\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::mongodb::bson::{doc, to_document};\nuse formatting::format_serror;\nuse komodo_client::{\n  api::write::*,\n  entities::{\n    FileContents, NoData, Operation, RepoExecutionArgs,\n    all_logs_success,\n    config::core::CoreConfig,\n    permission::PermissionLevel,\n    repo::Repo,\n    server::ServerState,\n    stack::{PartialStackConfig, Stack, StackInfo},\n    update::Update,\n    user::stack_user,\n  },\n};\nuse octorust::types::{\n  ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,\n};\nuse periphery_client::api::compose::{\n  GetComposeContentsOnHost, GetComposeContentsOnHostResponse,\n  WriteComposeContentsToHost,\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  config::core_config,\n  helpers::{\n    periphery_client,\n    query::get_server_with_state,\n    stack_git_token,\n    update::{add_update, make_update},\n  },\n  permission::get_check_permissions,\n  resource,\n  stack::{\n    remote::{RemoteComposeContents, get_repo_compose_contents},\n    services::extract_services_into_res,\n  },\n  state::{db_client, github_client},\n};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateStack {\n  #[instrument(name = \"CreateStack\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Stack> {\n    resource::create::<Stack>(&self.name, self.config, user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for CopyStack {\n  #[instrument(name = \"CopyStack\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Stack> {\n    let Stack { config, .. } = get_check_permissions::<Stack>(\n      &self.id,\n      user,\n      PermissionLevel::Read.into(),\n    )\n    .await?;\n\n    resource::create::<Stack>(&self.name, config.into(), user).await\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteStack {\n  #[instrument(name = \"DeleteStack\", skip(args))]\n  async fn resolve(self, args: &WriteArgs) -> serror::Result<Stack> {\n    Ok(resource::delete::<Stack>(&self.id, args).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateStack {\n  #[instrument(name = \"UpdateStack\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Stack> {\n    Ok(resource::update::<Stack>(&self.id, self.config, user).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameStack {\n  #[instrument(name = \"RenameStack\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    Ok(resource::rename::<Stack>(&self.id, &self.name, user).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for WriteStackFileContents {\n  #[instrument(name = \"WriteStackFileContents\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    let WriteStackFileContents {\n      stack,\n      file_path,\n      contents,\n    } = self;\n    let stack = get_check_permissions::<Stack>(\n      &stack,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if !stack.config.files_on_host\n      && stack.config.repo.is_empty()\n      && stack.config.linked_repo.is_empty()\n    {\n      return Err(anyhow!(\n        \"Stack is not configured to use Files on Host, Git Repo, or Linked Repo, can't write file contents\"\n      ).into());\n    }\n\n    let mut update =\n      make_update(&stack, Operation::WriteStackContents, user);\n\n    update.push_simple_log(\"File contents to write\", &contents);\n\n    if stack.config.files_on_host {\n      write_stack_file_contents_on_host(\n        stack, file_path, contents, update,\n      )\n      .await\n    } else {\n      write_stack_file_contents_git(\n        stack,\n        &file_path,\n        &contents,\n        &user.username,\n        update,\n      )\n      .await\n    }\n  }\n}\n\nasync fn write_stack_file_contents_on_host(\n  stack: Stack,\n  file_path: String,\n  contents: String,\n  mut update: Update,\n) -> serror::Result<Update> {\n  if stack.config.server_id.is_empty() {\n    return Err(anyhow!(\n      \"Cannot write file, Files on host Stack has not configured a Server\"\n    ).into());\n  }\n  let (server, state) =\n    get_server_with_state(&stack.config.server_id).await?;\n  if state != ServerState::Ok {\n    return Err(\n      anyhow!(\n        \"Cannot write file when server is unreachable or disabled\"\n      )\n      .into(),\n    );\n  }\n  match periphery_client(&server)?\n    .request(WriteComposeContentsToHost {\n      name: stack.name,\n      run_directory: stack.config.run_directory,\n      file_path,\n      contents,\n    })\n    .await\n    .context(\"Failed to write contents to host\")\n  {\n    Ok(log) => {\n      update.logs.push(log);\n    }\n    Err(e) => {\n      update.push_error_log(\n        \"Write File Contents\",\n        format_serror(&e.into()),\n      );\n    }\n  };\n\n  if !all_logs_success(&update.logs) {\n    update.finalize();\n    update.id = add_update(update.clone()).await?;\n    return Ok(update);\n  }\n\n  // Finish with a cache refresh\n  if let Err(e) = (RefreshStackCache { stack: stack.id })\n    .resolve(&WriteArgs {\n      user: stack_user().to_owned(),\n    })\n    .await\n    .map_err(|e| e.error)\n    .context(\n      \"Failed to refresh stack cache after writing file contents\",\n    )\n  {\n    update.push_error_log(\n      \"Refresh stack cache\",\n      format_serror(&e.into()),\n    );\n  }\n\n  update.finalize();\n  update.id = add_update(update.clone()).await?;\n\n  Ok(update)\n}\n\nasync fn write_stack_file_contents_git(\n  mut stack: Stack,\n  file_path: &str,\n  contents: &str,\n  username: &str,\n  mut update: Update,\n) -> serror::Result<Update> {\n  let mut repo = if !stack.config.linked_repo.is_empty() {\n    crate::resource::get::<Repo>(&stack.config.linked_repo)\n      .await?\n      .into()\n  } else {\n    None\n  };\n  let git_token = stack_git_token(&mut stack, repo.as_mut()).await?;\n\n  let mut repo_args: RepoExecutionArgs = if let Some(repo) = &repo {\n    repo.into()\n  } else {\n    (&stack).into()\n  };\n  let root = repo_args.unique_path(&core_config().repo_directory)?;\n  repo_args.destination = Some(root.display().to_string());\n\n  let file_path = stack\n    .config\n    .run_directory\n    .parse::<PathBuf>()\n    .context(\"Run directory is not a valid path\")?\n    .join(file_path);\n  let full_path =\n    root.join(&file_path).components().collect::<PathBuf>();\n\n  if let Some(parent) = full_path.parent() {\n    tokio::fs::create_dir_all(parent).await.with_context(|| {\n      format!(\n        \"Failed to initialize stack file parent directory {parent:?}\"\n      )\n    })?;\n  }\n\n  // Ensure the folder is initialized as git repo.\n  // This allows a new file to be committed on a branch that may not exist.\n  if !root.join(\".git\").exists() {\n    git::init_folder_as_repo(\n      &root,\n      &repo_args,\n      git_token.as_deref(),\n      &mut update.logs,\n    )\n    .await;\n\n    if !all_logs_success(&update.logs) {\n      update.finalize();\n      update.id = add_update(update.clone()).await?;\n      return Ok(update);\n    }\n  }\n\n  // Save this for later -- repo_args moved next.\n  let branch = repo_args.branch.clone();\n  // Pull latest changes to repo to ensure linear commit history\n  match git::pull_or_clone(\n    repo_args,\n    &core_config().repo_directory,\n    git_token,\n  )\n  .await\n  .context(\"Failed to pull latest changes before commit\")\n  {\n    Ok((res, _)) => update.logs.extend(res.logs),\n    Err(e) => {\n      update.push_error_log(\"Pull Repo\", format_serror(&e.into()));\n      update.finalize();\n      return Ok(update);\n    }\n  };\n\n  if !all_logs_success(&update.logs) {\n    update.finalize();\n    update.id = add_update(update.clone()).await?;\n    return Ok(update);\n  }\n\n  if let Err(e) = tokio::fs::write(&full_path, &contents)\n    .await\n    .with_context(|| {\n      format!(\n        \"Failed to write compose file contents to {full_path:?}\"\n      )\n    })\n  {\n    update.push_error_log(\"Write File\", format_serror(&e.into()));\n  } else {\n    update.push_simple_log(\n      \"Write File\",\n      format!(\"File written to {full_path:?}\"),\n    );\n  };\n\n  if !all_logs_success(&update.logs) {\n    update.finalize();\n    update.id = add_update(update.clone()).await?;\n\n    return Ok(update);\n  }\n\n  let commit_res = git::commit_file(\n    &format!(\"{username}: Write Stack File\"),\n    &root,\n    &file_path,\n    &branch,\n  )\n  .await;\n\n  update.logs.extend(commit_res.logs);\n\n  // Finish with a cache refresh\n  if let Err(e) = (RefreshStackCache { stack: stack.id })\n    .resolve(&WriteArgs {\n      user: stack_user().to_owned(),\n    })\n    .await\n    .map_err(|e| e.error)\n    .context(\n      \"Failed to refresh stack cache after writing file contents\",\n    )\n  {\n    update.push_error_log(\n      \"Refresh stack cache\",\n      format_serror(&e.into()),\n    );\n  }\n\n  update.finalize();\n  update.id = add_update(update.clone()).await?;\n\n  Ok(update)\n}\n\nimpl Resolve<WriteArgs> for RefreshStackCache {\n  #[instrument(\n    name = \"RefreshStackCache\",\n    level = \"debug\",\n    skip(user)\n  )]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<NoData> {\n    // Even though this is a write request, this doesn't change any config. Anyone that can execute the\n    // stack should be able to do this.\n    let stack = get_check_permissions::<Stack>(\n      &self.stack,\n      user,\n      PermissionLevel::Execute.into(),\n    )\n    .await?;\n\n    let repo = if !stack.config.files_on_host\n      && !stack.config.linked_repo.is_empty()\n    {\n      crate::resource::get::<Repo>(&stack.config.linked_repo)\n        .await?\n        .into()\n    } else {\n      None\n    };\n\n    let file_contents_empty = stack.config.file_contents.is_empty();\n    let repo_empty =\n      stack.config.repo.is_empty() && repo.as_ref().is_none();\n\n    if !stack.config.files_on_host\n      && file_contents_empty\n      && repo_empty\n    {\n      // Nothing to do without one of these\n      return Ok(NoData {});\n    }\n\n    let mut missing_files = Vec::new();\n\n    let (\n      latest_services,\n      remote_contents,\n      remote_errors,\n      latest_hash,\n      latest_message,\n    ) = if stack.config.files_on_host {\n      // =============\n      // FILES ON HOST\n      // =============\n      let (server, state) = if stack.config.server_id.is_empty() {\n        (None, ServerState::Disabled)\n      } else {\n        let (server, state) =\n          get_server_with_state(&stack.config.server_id).await?;\n        (Some(server), state)\n      };\n      if state != ServerState::Ok {\n        (vec![], None, None, None, None)\n      } else if let Some(server) = server {\n        let GetComposeContentsOnHostResponse { contents, errors } =\n          match periphery_client(&server)?\n            .request(GetComposeContentsOnHost {\n              file_paths: stack.all_file_dependencies(),\n              name: stack.name.clone(),\n              run_directory: stack.config.run_directory.clone(),\n            })\n            .await\n            .context(\"failed to get compose file contents from host\")\n          {\n            Ok(res) => res,\n            Err(e) => GetComposeContentsOnHostResponse {\n              contents: Default::default(),\n              errors: vec![FileContents {\n                path: stack.config.run_directory.clone(),\n                contents: format_serror(&e.into()),\n              }],\n            },\n          };\n\n        let project_name = stack.project_name(true);\n\n        let mut services = Vec::new();\n\n        for contents in &contents {\n          // Don't include additional files in service parsing\n          if !stack.is_compose_file(&contents.path) {\n            continue;\n          }\n          if let Err(e) = extract_services_into_res(\n            &project_name,\n            &contents.contents,\n            &mut services,\n          ) {\n            warn!(\n              \"failed to extract stack services, things won't works correctly. stack: {} | {e:#}\",\n              stack.name\n            );\n          }\n        }\n\n        (services, Some(contents), Some(errors), None, None)\n      } else {\n        (vec![], None, None, None, None)\n      }\n    } else if !repo_empty {\n      // ================\n      // REPO BASED STACK\n      // ================\n      let RemoteComposeContents {\n        successful: remote_contents,\n        errored: remote_errors,\n        hash: latest_hash,\n        message: latest_message,\n        ..\n      } = get_repo_compose_contents(\n        &stack,\n        repo.as_ref(),\n        Some(&mut missing_files),\n      )\n      .await?;\n\n      let project_name = stack.project_name(true);\n\n      let mut services = Vec::new();\n\n      for contents in &remote_contents {\n        // Don't include additional files in service parsing\n        if !stack.is_compose_file(&contents.path) {\n          continue;\n        }\n        if let Err(e) = extract_services_into_res(\n          &project_name,\n          &contents.contents,\n          &mut services,\n        ) {\n          warn!(\n            \"failed to extract stack services, things won't works correctly. stack: {} | {e:#}\",\n            stack.name\n          );\n        }\n      }\n\n      (\n        services,\n        Some(remote_contents),\n        Some(remote_errors),\n        latest_hash,\n        latest_message,\n      )\n    } else {\n      // =============\n      // UI BASED FILE\n      // =============\n      let mut services = Vec::new();\n      if let Err(e) = extract_services_into_res(\n        // this should latest (not deployed), so make the project name fresh.\n        &stack.project_name(true),\n        &stack.config.file_contents,\n        &mut services,\n      ) {\n        warn!(\n          \"Failed to extract Stack services for {}, things may not work correctly. | {e:#}\",\n          stack.name\n        );\n        services.extend(stack.info.latest_services.clone());\n      };\n      (services, None, None, None, None)\n    };\n\n    let info = StackInfo {\n      missing_files,\n      deployed_services: stack.info.deployed_services.clone(),\n      deployed_project_name: stack.info.deployed_project_name.clone(),\n      deployed_contents: stack.info.deployed_contents.clone(),\n      deployed_config: stack.info.deployed_config.clone(),\n      deployed_hash: stack.info.deployed_hash.clone(),\n      deployed_message: stack.info.deployed_message.clone(),\n      latest_services,\n      remote_contents,\n      remote_errors,\n      latest_hash,\n      latest_message,\n    };\n\n    let info = to_document(&info)\n      .context(\"failed to serialize stack info to bson\")?;\n\n    db_client()\n      .stacks\n      .update_one(\n        doc! { \"name\": &stack.name },\n        doc! { \"$set\": { \"info\": info } },\n      )\n      .await\n      .context(\"failed to update stack info on db\")?;\n\n    Ok(NoData {})\n  }\n}\n\nimpl Resolve<WriteArgs> for CreateStackWebhook {\n  #[instrument(name = \"CreateStackWebhook\", skip(args))]\n  async fn resolve(\n    self,\n    args: &WriteArgs,\n  ) -> serror::Result<CreateStackWebhookResponse> {\n    let WriteArgs { user } = args;\n\n    let Some(github) = github_client() else {\n      return Err(\n        anyhow!(\n          \"github_webhook_app is not configured in core config toml\"\n        )\n        .into(),\n      );\n    };\n\n    let stack = get_check_permissions::<Stack>(\n      &self.stack,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if stack.config.repo.is_empty() {\n      return Err(\n        anyhow!(\"No repo configured, can't create webhook\").into(),\n      );\n    }\n\n    let mut split = stack.config.repo.split('/');\n    let owner = split.next().context(\"Stack repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Err(\n        anyhow!(\"Cannot manage repo webhooks under owner {owner}\")\n          .into(),\n      );\n    };\n\n    let repo =\n      split.next().context(\"Stack repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    // First make sure the webhook isn't already created (inactive ones are ignored)\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      webhook_secret,\n      ..\n    } = core_config();\n\n    let webhook_secret = if stack.config.webhook_secret.is_empty() {\n      webhook_secret\n    } else {\n      &stack.config.webhook_secret\n    };\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let url = match self.action {\n      StackWebhookAction::Refresh => {\n        format!(\"{host}/listener/github/stack/{}/refresh\", stack.id)\n      }\n      StackWebhookAction::Deploy => {\n        format!(\"{host}/listener/github/stack/{}/deploy\", stack.id)\n      }\n    };\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == url {\n        return Ok(NoData {});\n      }\n    }\n\n    // Now good to create the webhook\n    let request = ReposCreateWebhookRequest {\n      active: Some(true),\n      config: Some(ReposCreateWebhookRequestConfig {\n        url,\n        secret: webhook_secret.to_string(),\n        content_type: String::from(\"json\"),\n        insecure_ssl: None,\n        digest: Default::default(),\n        token: Default::default(),\n      }),\n      events: vec![String::from(\"push\")],\n      name: String::from(\"web\"),\n    };\n    github_repos\n      .create_webhook(owner, repo, &request)\n      .await\n      .context(\"failed to create webhook\")?;\n\n    if !stack.config.webhook_enabled {\n      UpdateStack {\n        id: stack.id,\n        config: PartialStackConfig {\n          webhook_enabled: Some(true),\n          ..Default::default()\n        },\n      }\n      .resolve(args)\n      .await\n      .map_err(|e| e.error)\n      .context(\"failed to update stack to enable webhook\")?;\n    }\n\n    Ok(NoData {})\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteStackWebhook {\n  #[instrument(name = \"DeleteStackWebhook\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<DeleteStackWebhookResponse> {\n    let Some(github) = github_client() else {\n      return Err(\n        anyhow!(\n          \"github_webhook_app is not configured in core config toml\"\n        )\n        .into(),\n      );\n    };\n\n    let stack = get_check_permissions::<Stack>(\n      &self.stack,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if stack.config.git_provider != \"github.com\" {\n      return Err(\n        anyhow!(\"Can only manage github.com repo webhooks\").into(),\n      );\n    }\n\n    if stack.config.repo.is_empty() {\n      return Err(\n        anyhow!(\"No repo configured, can't create webhook\").into(),\n      );\n    }\n\n    let mut split = stack.config.repo.split('/');\n    let owner = split.next().context(\"Stack repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Err(\n        anyhow!(\"Cannot manage repo webhooks under owner {owner}\")\n          .into(),\n      );\n    };\n\n    let repo =\n      split.next().context(\"Sync repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    // First make sure the webhook isn't already created (inactive ones are ignored)\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      ..\n    } = core_config();\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let url = match self.action {\n      StackWebhookAction::Refresh => {\n        format!(\"{host}/listener/github/stack/{}/refresh\", stack.id)\n      }\n      StackWebhookAction::Deploy => {\n        format!(\"{host}/listener/github/stack/{}/deploy\", stack.id)\n      }\n    };\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == url {\n        github_repos\n          .delete_webhook(owner, repo, webhook.id)\n          .await\n          .context(\"failed to delete webhook\")?;\n        return Ok(NoData {});\n      }\n    }\n\n    // No webhook to delete, all good\n    Ok(NoData {})\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/sync.rs",
    "content": "use std::{\n  collections::HashMap,\n  path::{Path, PathBuf},\n};\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::update_one_by_id,\n  mongodb::bson::{doc, to_document},\n};\nuse formatting::format_serror;\nuse komodo_client::{\n  api::{read::ExportAllResourcesToToml, write::*},\n  entities::{\n    self, NoData, Operation, RepoExecutionArgs, ResourceTarget,\n    action::Action,\n    alert::{Alert, AlertData, SeverityLevel},\n    alerter::Alerter,\n    all_logs_success,\n    build::Build,\n    builder::Builder,\n    config::core::CoreConfig,\n    deployment::Deployment,\n    komodo_timestamp,\n    permission::PermissionLevel,\n    procedure::Procedure,\n    repo::Repo,\n    server::Server,\n    stack::Stack,\n    sync::{\n      PartialResourceSyncConfig, ResourceSync, ResourceSyncInfo,\n      SyncDeployUpdate,\n    },\n    to_path_compatible_name,\n    update::{Log, Update},\n    user::sync_user,\n  },\n};\nuse octorust::types::{\n  ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  alert::send_alerts,\n  api::read::ReadArgs,\n  config::core_config,\n  helpers::{\n    all_resources::AllResourcesById,\n    git_token,\n    query::get_id_to_tags,\n    update::{add_update, make_update, update_update},\n  },\n  permission::get_check_permissions,\n  resource,\n  state::{db_client, github_client},\n  sync::{\n    deploy::SyncDeployParams, remote::RemoteResources,\n    view::push_updates_for_view,\n  },\n};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateResourceSync {\n  #[instrument(name = \"CreateResourceSync\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<ResourceSync> {\n    resource::create::<ResourceSync>(&self.name, self.config, user)\n      .await\n  }\n}\n\nimpl Resolve<WriteArgs> for CopyResourceSync {\n  #[instrument(name = \"CopyResourceSync\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<ResourceSync> {\n    let ResourceSync { config, .. } =\n      get_check_permissions::<ResourceSync>(\n        &self.id,\n        user,\n        PermissionLevel::Write.into(),\n      )\n      .await?;\n    resource::create::<ResourceSync>(&self.name, config.into(), user)\n      .await\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteResourceSync {\n  #[instrument(name = \"DeleteResourceSync\", skip(args))]\n  async fn resolve(\n    self,\n    args: &WriteArgs,\n  ) -> serror::Result<ResourceSync> {\n    Ok(resource::delete::<ResourceSync>(&self.id, args).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateResourceSync {\n  #[instrument(name = \"UpdateResourceSync\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<ResourceSync> {\n    Ok(\n      resource::update::<ResourceSync>(&self.id, self.config, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameResourceSync {\n  #[instrument(name = \"RenameResourceSync\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Update> {\n    Ok(\n      resource::rename::<ResourceSync>(&self.id, &self.name, user)\n        .await?,\n    )\n  }\n}\n\nimpl Resolve<WriteArgs> for WriteSyncFileContents {\n  #[instrument(name = \"WriteSyncFileContents\", skip(args))]\n  async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {\n    let sync = get_check_permissions::<ResourceSync>(\n      &self.sync,\n      &args.user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    let repo = if !sync.config.files_on_host\n      && !sync.config.linked_repo.is_empty()\n    {\n      crate::resource::get::<Repo>(&sync.config.linked_repo)\n        .await?\n        .into()\n    } else {\n      None\n    };\n\n    if !sync.config.files_on_host\n      && sync.config.repo.is_empty()\n      && sync.config.linked_repo.is_empty()\n    {\n      return Err(\n        anyhow!(\n          \"This method is only for 'files on host' or 'repo' based syncs.\"\n        )\n        .into(),\n      );\n    }\n\n    let mut update =\n      make_update(&sync, Operation::WriteSyncContents, &args.user);\n\n    update.push_simple_log(\"File contents\", &self.contents);\n\n    if sync.config.files_on_host {\n      write_sync_file_contents_on_host(self, args, sync, update).await\n    } else {\n      write_sync_file_contents_git(self, args, sync, repo, update)\n        .await\n    }\n  }\n}\n\nasync fn write_sync_file_contents_on_host(\n  req: WriteSyncFileContents,\n  args: &WriteArgs,\n  sync: ResourceSync,\n  mut update: Update,\n) -> serror::Result<Update> {\n  let WriteSyncFileContents {\n    sync: _,\n    resource_path,\n    file_path,\n    contents,\n  } = req;\n\n  let root = core_config()\n    .sync_directory\n    .join(to_path_compatible_name(&sync.name));\n  let file_path =\n    file_path.parse::<PathBuf>().context(\"Invalid file path\")?;\n  let resource_path = resource_path\n    .parse::<PathBuf>()\n    .context(\"Invalid resource path\")?;\n  let full_path = root.join(&resource_path).join(&file_path);\n\n  if let Some(parent) = full_path.parent() {\n    tokio::fs::create_dir_all(parent).await.with_context(|| {\n      format!(\n        \"Failed to initialize resource file parent directory {parent:?}\"\n      )\n    })?;\n  }\n\n  if let Err(e) = tokio::fs::write(&full_path, &contents)\n    .await\n    .with_context(|| {\n      format!(\n        \"Failed to write resource file contents to {full_path:?}\"\n      )\n    })\n  {\n    update.push_error_log(\"Write File\", format_serror(&e.into()));\n  } else {\n    update.push_simple_log(\n      \"Write File\",\n      format!(\"File written to {full_path:?}\"),\n    );\n  };\n\n  if !all_logs_success(&update.logs) {\n    update.finalize();\n    update.id = add_update(update.clone()).await?;\n\n    return Ok(update);\n  }\n\n  if let Err(e) = (RefreshResourceSyncPending { sync: sync.name })\n    .resolve(args)\n    .await\n  {\n    update.push_error_log(\n      \"Refresh failed\",\n      format_serror(&e.error.into()),\n    );\n  }\n\n  update.finalize();\n  update.id = add_update(update.clone()).await?;\n\n  Ok(update)\n}\n\nasync fn write_sync_file_contents_git(\n  req: WriteSyncFileContents,\n  args: &WriteArgs,\n  sync: ResourceSync,\n  repo: Option<Repo>,\n  mut update: Update,\n) -> serror::Result<Update> {\n  let WriteSyncFileContents {\n    sync: _,\n    resource_path,\n    file_path,\n    contents,\n  } = req;\n\n  let mut repo_args: RepoExecutionArgs = if let Some(repo) = &repo {\n    repo.into()\n  } else {\n    (&sync).into()\n  };\n  let root = repo_args.unique_path(&core_config().repo_directory)?;\n  repo_args.destination = Some(root.display().to_string());\n\n  let git_token = if let Some(account) = &repo_args.account {\n    git_token(&repo_args.provider, account, |https| repo_args.https = https)\n    .await\n    .with_context(\n      || format!(\"Failed to get git token in call to db. Stopping run. | {} | {account}\", repo_args.provider),\n    )?\n  } else {\n    None\n  };\n\n  let file_path =\n    file_path.parse::<PathBuf>().with_context(|| {\n      format!(\"File path is not a valid path: {file_path}\")\n    })?;\n  let resource_path =\n    resource_path.parse::<PathBuf>().with_context(|| {\n      format!(\"Resource path is not a valid path: {resource_path}\")\n    })?;\n  let full_path = root\n    .join(&resource_path)\n    .join(&file_path)\n    .components()\n    .collect::<PathBuf>();\n\n  if let Some(parent) = full_path.parent() {\n    tokio::fs::create_dir_all(parent).await.with_context(|| {\n      format!(\n        \"Failed to initialize resource file parent directory {parent:?}\"\n      )\n    })?;\n  }\n\n  // Ensure the folder is initialized as git repo.\n  // This allows a new file to be committed on a branch that may not exist.\n  if !root.join(\".git\").exists() {\n    git::init_folder_as_repo(\n      &root,\n      &repo_args,\n      git_token.as_deref(),\n      &mut update.logs,\n    )\n    .await;\n\n    if !all_logs_success(&update.logs) {\n      update.finalize();\n      update.id = add_update(update.clone()).await?;\n      return Ok(update);\n    }\n  }\n\n  // Save this for later -- repo_args moved next.\n  let branch = repo_args.branch.clone();\n  // Pull latest changes to repo to ensure linear commit history\n  match git::pull_or_clone(\n    repo_args,\n    &core_config().repo_directory,\n    git_token,\n  )\n  .await\n  .context(\"Failed to pull latest changes before commit\")\n  {\n    Ok((res, _)) => update.logs.extend(res.logs),\n    Err(e) => {\n      update.push_error_log(\"Pull Repo\", format_serror(&e.into()));\n      update.finalize();\n      return Ok(update);\n    }\n  };\n\n  if !all_logs_success(&update.logs) {\n    update.finalize();\n    update.id = add_update(update.clone()).await?;\n    return Ok(update);\n  }\n\n  if let Err(e) = tokio::fs::write(&full_path, &contents)\n    .await\n    .with_context(|| {\n      format!(\n        \"Failed to write resource file contents to {full_path:?}\"\n      )\n    })\n  {\n    update.push_error_log(\"Write File\", format_serror(&e.into()));\n  } else {\n    update.push_simple_log(\n      \"Write File\",\n      format!(\"File written to {full_path:?}\"),\n    );\n  };\n\n  if !all_logs_success(&update.logs) {\n    update.finalize();\n    update.id = add_update(update.clone()).await?;\n\n    return Ok(update);\n  }\n\n  let commit_res = git::commit_file(\n    &format!(\"{}: Commit Resource File\", args.user.username),\n    &root,\n    &resource_path.join(&file_path),\n    &branch,\n  )\n  .await;\n\n  update.logs.extend(commit_res.logs);\n\n  if let Err(e) = (RefreshResourceSyncPending { sync: sync.name })\n    .resolve(args)\n    .await\n    .map_err(|e| e.error)\n    .context(\n      \"Failed to refresh sync pending after writing file contents\",\n    )\n  {\n    update.push_error_log(\n      \"Refresh sync pending\",\n      format_serror(&e.into()),\n    );\n  }\n\n  update.finalize();\n  update.id = add_update(update.clone()).await?;\n\n  Ok(update)\n}\n\nimpl Resolve<WriteArgs> for CommitSync {\n  #[instrument(name = \"CommitSync\", skip(args))]\n  async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {\n    let WriteArgs { user } = args;\n\n    let sync = get_check_permissions::<entities::sync::ResourceSync>(\n      &self.sync,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    let repo = if !sync.config.files_on_host\n      && !sync.config.linked_repo.is_empty()\n    {\n      crate::resource::get::<Repo>(&sync.config.linked_repo)\n        .await?\n        .into()\n    } else {\n      None\n    };\n\n    let file_contents_empty = sync.config.file_contents_empty();\n\n    let fresh_sync = !sync.config.files_on_host\n      && sync.config.repo.is_empty()\n      && repo.is_none()\n      && file_contents_empty;\n\n    if !sync.config.managed && !fresh_sync {\n      return Err(\n        anyhow!(\"Cannot commit to sync. Enabled 'managed' mode.\")\n          .into(),\n      );\n    }\n\n    // Get this here so it can fail before update created.\n    let resource_path = if sync.config.files_on_host\n      || !sync.config.repo.is_empty()\n      || repo.is_some()\n    {\n      let resource_path = sync\n        .config\n        .resource_path\n        .first()\n        .context(\"Sync does not have resource path configured.\")?\n        .parse::<PathBuf>()\n        .context(\"Invalid resource path\")?;\n\n      if resource_path\n        .extension()\n        .context(\"Resource path missing '.toml' extension\")?\n        != \"toml\"\n      {\n        return Err(\n          anyhow!(\"Resource path missing '.toml' extension\").into(),\n        );\n      }\n      Some(resource_path)\n    } else {\n      None\n    };\n\n    let res = ExportAllResourcesToToml {\n      include_resources: sync.config.include_resources,\n      tags: sync.config.match_tags.clone(),\n      include_variables: sync.config.include_variables,\n      include_user_groups: sync.config.include_user_groups,\n    }\n    .resolve(&ReadArgs {\n      user: sync_user().to_owned(),\n    })\n    .await?;\n\n    let mut update = make_update(&sync, Operation::CommitSync, user);\n    update.id = add_update(update.clone()).await?;\n\n    update.logs.push(Log::simple(\"Resources\", res.toml.clone()));\n\n    if sync.config.files_on_host {\n      let Some(resource_path) = resource_path else {\n        // Resource path checked above for files_on_host mode.\n        unreachable!()\n      };\n      let file_path = core_config()\n        .sync_directory\n        .join(to_path_compatible_name(&sync.name))\n        .join(&resource_path);\n      if let Some(parent) = file_path.parent() {\n        tokio::fs::create_dir_all(parent)\n          .await\n          .with_context(|| format!(\"Failed to initialize resource file parent directory {parent:?}\"))?;\n      };\n      if let Err(e) = tokio::fs::write(&file_path, &res.toml)\n        .await\n        .with_context(|| {\n          format!(\"Failed to write resource file to {file_path:?}\",)\n        })\n      {\n        update.push_error_log(\n          \"Write resource file\",\n          format_serror(&e.into()),\n        );\n        update.finalize();\n        add_update(update.clone()).await?;\n        return Ok(update);\n      } else {\n        update.push_simple_log(\n          \"Write contents\",\n          format!(\"File contents written to {file_path:?}\"),\n        );\n      }\n    } else if let Some(repo) = &repo {\n      let Some(resource_path) = resource_path else {\n        // Resource path checked above for repo mode.\n        unreachable!()\n      };\n      let args: RepoExecutionArgs = repo.into();\n      if let Err(e) =\n        commit_git_sync(args, &resource_path, &res.toml, &mut update)\n          .await\n      {\n        update.push_error_log(\n          \"Write resource file\",\n          format_serror(&e.into()),\n        );\n        update.finalize();\n        add_update(update.clone()).await?;\n        return Ok(update);\n      }\n    } else if !sync.config.repo.is_empty() {\n      let Some(resource_path) = resource_path else {\n        // Resource path checked above for repo mode.\n        unreachable!()\n      };\n      let args: RepoExecutionArgs = (&sync).into();\n      if let Err(e) =\n        commit_git_sync(args, &resource_path, &res.toml, &mut update)\n          .await\n      {\n        update.push_error_log(\n          \"Write resource file\",\n          format_serror(&e.into()),\n        );\n        update.finalize();\n        add_update(update.clone()).await?;\n        return Ok(update);\n      }\n\n      // ===========\n      // UI DEFINED\n    } else if let Err(e) = db_client()\n      .resource_syncs\n      .update_one(\n        doc! { \"name\": &sync.name },\n        doc! { \"$set\": { \"config.file_contents\": res.toml } },\n      )\n      .await\n      .context(\"failed to update file_contents on db\")\n    {\n      update.push_error_log(\n        \"Write resource to database\",\n        format_serror(&e.into()),\n      );\n      update.finalize();\n      add_update(update.clone()).await?;\n      return Ok(update);\n    }\n\n    if let Err(e) = (RefreshResourceSyncPending { sync: sync.name })\n      .resolve(args)\n      .await\n    {\n      update.push_error_log(\n        \"Refresh sync pending\",\n        format_serror(&e.error.into()),\n      );\n    };\n\n    update.finalize();\n    update_update(update.clone()).await?;\n\n    Ok(update)\n  }\n}\n\nasync fn commit_git_sync(\n  mut args: RepoExecutionArgs,\n  resource_path: &Path,\n  toml: &str,\n  update: &mut Update,\n) -> anyhow::Result<()> {\n  let root = args.unique_path(&core_config().repo_directory)?;\n  args.destination = Some(root.display().to_string());\n\n  let access_token = if let Some(account) = &args.account {\n    git_token(&args.provider, account, |https| args.https = https)\n      .await\n      .with_context(\n        || format!(\"Failed to get git token in call to db. Stopping run. | {} | {account}\", args.provider),\n      )?\n  } else {\n    None\n  };\n\n  let (pull_res, _) = git::pull_or_clone(\n    args.clone(),\n    &core_config().repo_directory,\n    access_token,\n  )\n  .await?;\n  update.logs.extend(pull_res.logs);\n  if !all_logs_success(&update.logs) {\n    return Ok(());\n  }\n\n  let res = git::write_commit_file(\n    \"Commit Sync\",\n    &root,\n    resource_path,\n    toml,\n    &args.branch,\n  )\n  .await?;\n  update.logs.extend(res.logs);\n\n  Ok(())\n}\n\nimpl Resolve<WriteArgs> for RefreshResourceSyncPending {\n  #[instrument(\n    name = \"RefreshResourceSyncPending\",\n    level = \"debug\",\n    skip(user)\n  )]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<ResourceSync> {\n    // Even though this is a write request, this doesn't change any config. Anyone that can execute the\n    // sync should be able to do this.\n    let mut sync =\n      get_check_permissions::<entities::sync::ResourceSync>(\n        &self.sync,\n        user,\n        PermissionLevel::Execute.into(),\n      )\n      .await?;\n\n    let repo = if !sync.config.files_on_host\n      && !sync.config.linked_repo.is_empty()\n    {\n      crate::resource::get::<Repo>(&sync.config.linked_repo)\n        .await?\n        .into()\n    } else {\n      None\n    };\n\n    if !sync.config.managed\n      && !sync.config.files_on_host\n      && sync.config.file_contents.is_empty()\n      && sync.config.repo.is_empty()\n      && sync.config.linked_repo.is_empty()\n    {\n      // Sync not configured, nothing to refresh\n      return Ok(sync);\n    }\n\n    let res = async {\n      let RemoteResources {\n        resources,\n        files,\n        file_errors,\n        hash,\n        message,\n        ..\n      } = crate::sync::remote::get_remote_resources(\n        &sync,\n        repo.as_ref(),\n      )\n      .await\n      .context(\"failed to get remote resources\")?;\n\n      sync.info.remote_contents = files;\n      sync.info.remote_errors = file_errors;\n      sync.info.pending_hash = hash;\n      sync.info.pending_message = message;\n\n      if !sync.info.remote_errors.is_empty() {\n        return Err(anyhow!(\n          \"Remote resources have errors. Cannot compute diffs.\"\n        ));\n      }\n\n      let resources = resources?;\n      let delete = sync.config.managed || sync.config.delete;\n      let all_resources = AllResourcesById::load().await?;\n\n      let (resource_updates, deploy_updates) =\n        if sync.config.include_resources {\n          let id_to_tags = get_id_to_tags(None).await?;\n\n          let deployments_by_name = all_resources\n            .deployments\n            .values()\n            .map(|deployment| {\n              (deployment.name.clone(), deployment.clone())\n            })\n            .collect::<HashMap<_, _>>();\n          let stacks_by_name = all_resources\n            .stacks\n            .values()\n            .map(|stack| (stack.name.clone(), stack.clone()))\n            .collect::<HashMap<_, _>>();\n\n          let deploy_updates =\n            crate::sync::deploy::get_updates_for_view(\n              SyncDeployParams {\n                deployments: &resources.deployments,\n                deployment_map: &deployments_by_name,\n                stacks: &resources.stacks,\n                stack_map: &stacks_by_name,\n              },\n            )\n            .await;\n\n          let mut diffs = Vec::new();\n\n          push_updates_for_view::<Server>(\n            resources.servers,\n            delete,\n            None,\n            None,\n            &id_to_tags,\n            &sync.config.match_tags,\n            &mut diffs,\n          )\n          .await?;\n          push_updates_for_view::<Stack>(\n            resources.stacks,\n            delete,\n            None,\n            None,\n            &id_to_tags,\n            &sync.config.match_tags,\n            &mut diffs,\n          )\n          .await?;\n          push_updates_for_view::<Deployment>(\n            resources.deployments,\n            delete,\n            None,\n            None,\n            &id_to_tags,\n            &sync.config.match_tags,\n            &mut diffs,\n          )\n          .await?;\n          push_updates_for_view::<Build>(\n            resources.builds,\n            delete,\n            None,\n            None,\n            &id_to_tags,\n            &sync.config.match_tags,\n            &mut diffs,\n          )\n          .await?;\n          push_updates_for_view::<Repo>(\n            resources.repos,\n            delete,\n            None,\n            None,\n            &id_to_tags,\n            &sync.config.match_tags,\n            &mut diffs,\n          )\n          .await?;\n          push_updates_for_view::<Procedure>(\n            resources.procedures,\n            delete,\n            None,\n            None,\n            &id_to_tags,\n            &sync.config.match_tags,\n            &mut diffs,\n          )\n          .await?;\n          push_updates_for_view::<Action>(\n            resources.actions,\n            delete,\n            None,\n            None,\n            &id_to_tags,\n            &sync.config.match_tags,\n            &mut diffs,\n          )\n          .await?;\n          push_updates_for_view::<Builder>(\n            resources.builders,\n            delete,\n            None,\n            None,\n            &id_to_tags,\n            &sync.config.match_tags,\n            &mut diffs,\n          )\n          .await?;\n          push_updates_for_view::<Alerter>(\n            resources.alerters,\n            delete,\n            None,\n            None,\n            &id_to_tags,\n            &sync.config.match_tags,\n            &mut diffs,\n          )\n          .await?;\n          push_updates_for_view::<ResourceSync>(\n            resources.resource_syncs,\n            delete,\n            None,\n            None,\n            &id_to_tags,\n            &sync.config.match_tags,\n            &mut diffs,\n          )\n          .await?;\n\n          (diffs, deploy_updates)\n        } else {\n          (Vec::new(), SyncDeployUpdate::default())\n        };\n\n      let variable_updates = if sync.config.include_variables {\n        crate::sync::variables::get_updates_for_view(\n          &resources.variables,\n          delete,\n        )\n        .await?\n      } else {\n        Default::default()\n      };\n\n      let user_group_updates = if sync.config.include_user_groups {\n        crate::sync::user_groups::get_updates_for_view(\n          resources.user_groups,\n          delete,\n        )\n        .await?\n      } else {\n        Default::default()\n      };\n\n      anyhow::Ok((\n        resource_updates,\n        deploy_updates,\n        variable_updates,\n        user_group_updates,\n      ))\n    }\n    .await;\n\n    let (\n      resource_updates,\n      deploy_updates,\n      variable_updates,\n      user_group_updates,\n      pending_error,\n    ) = match res {\n      Ok(res) => (res.0, res.1, res.2, res.3, None),\n      Err(e) => (\n        Default::default(),\n        Default::default(),\n        Default::default(),\n        Default::default(),\n        Some(format_serror(&e.into())),\n      ),\n    };\n\n    let has_updates = !resource_updates.is_empty()\n      || !deploy_updates.to_deploy == 0\n      || !variable_updates.is_empty()\n      || !user_group_updates.is_empty();\n\n    let info = ResourceSyncInfo {\n      last_sync_ts: sync.info.last_sync_ts,\n      last_sync_hash: sync.info.last_sync_hash,\n      last_sync_message: sync.info.last_sync_message,\n      remote_contents: sync.info.remote_contents,\n      remote_errors: sync.info.remote_errors,\n      pending_hash: sync.info.pending_hash,\n      pending_message: sync.info.pending_message,\n      pending_deploy: deploy_updates,\n      resource_updates,\n      variable_updates,\n      user_group_updates,\n      pending_error,\n    };\n\n    let info = to_document(&info)\n      .context(\"failed to serialize pending to document\")?;\n\n    update_one_by_id(\n      &db_client().resource_syncs,\n      &sync.id,\n      doc! { \"$set\": { \"info\": info } },\n      None,\n    )\n    .await?;\n\n    // check to update alert\n    let id = sync.id.clone();\n    let name = sync.name.clone();\n    tokio::task::spawn(async move {\n      let db = db_client();\n      let Some(existing) = db_client()\n        .alerts\n        .find_one(doc! {\n          \"resolved\": false,\n          \"target.type\": \"ResourceSync\",\n          \"target.id\": &id,\n        })\n        .await\n        .context(\"failed to query db for alert\")\n        .inspect_err(|e| warn!(\"{e:#}\"))\n        .ok()\n      else {\n        return;\n      };\n      match (existing, has_updates) {\n        // OPEN A NEW ALERT\n        (None, true) => {\n          let alert = Alert {\n            id: Default::default(),\n            ts: komodo_timestamp(),\n            resolved: false,\n            level: SeverityLevel::Ok,\n            target: ResourceTarget::ResourceSync(id.clone()),\n            data: AlertData::ResourceSyncPendingUpdates { id, name },\n            resolved_ts: None,\n          };\n          db.alerts\n            .insert_one(&alert)\n            .await\n            .context(\"failed to open existing pending resource sync updates alert\")\n            .inspect_err(|e| warn!(\"{e:#}\"))\n            .ok();\n          if sync.config.pending_alert {\n            send_alerts(&[alert]).await;\n          }\n        }\n        // CLOSE ALERT\n        (Some(existing), false) => {\n          update_one_by_id(\n            &db.alerts,\n            &existing.id,\n            doc! {\n              \"$set\": {\n                \"resolved\": true,\n                \"resolved_ts\": komodo_timestamp()\n              }\n            },\n            None,\n          )\n          .await\n          .context(\"failed to close existing pending resource sync updates alert\")\n          .inspect_err(|e| warn!(\"{e:#}\"))\n          .ok();\n        }\n        // NOTHING TO DO\n        _ => {}\n      }\n    });\n\n    Ok(crate::resource::get::<ResourceSync>(&sync.id).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for CreateSyncWebhook {\n  #[instrument(name = \"CreateSyncWebhook\", skip(args))]\n  async fn resolve(\n    self,\n    args: &WriteArgs,\n  ) -> serror::Result<CreateSyncWebhookResponse> {\n    let WriteArgs { user } = args;\n    let Some(github) = github_client() else {\n      return Err(\n        anyhow!(\n          \"github_webhook_app is not configured in core config toml\"\n        )\n        .into(),\n      );\n    };\n\n    let sync = get_check_permissions::<ResourceSync>(\n      &self.sync,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if sync.config.repo.is_empty() {\n      return Err(\n        anyhow!(\"No repo configured, can't create webhook\").into(),\n      );\n    }\n\n    let mut split = sync.config.repo.split('/');\n    let owner = split.next().context(\"Sync repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Err(\n        anyhow!(\"Cannot manage repo webhooks under owner {owner}\")\n          .into(),\n      );\n    };\n\n    let repo =\n      split.next().context(\"Repo repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    // First make sure the webhook isn't already created (inactive ones are ignored)\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      webhook_secret,\n      ..\n    } = core_config();\n\n    let webhook_secret = if sync.config.webhook_secret.is_empty() {\n      webhook_secret\n    } else {\n      &sync.config.webhook_secret\n    };\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let url = match self.action {\n      SyncWebhookAction::Refresh => {\n        format!(\"{host}/listener/github/sync/{}/refresh\", sync.id)\n      }\n      SyncWebhookAction::Sync => {\n        format!(\"{host}/listener/github/sync/{}/sync\", sync.id)\n      }\n    };\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == url {\n        return Ok(NoData {});\n      }\n    }\n\n    // Now good to create the webhook\n    let request = ReposCreateWebhookRequest {\n      active: Some(true),\n      config: Some(ReposCreateWebhookRequestConfig {\n        url,\n        secret: webhook_secret.to_string(),\n        content_type: String::from(\"json\"),\n        insecure_ssl: None,\n        digest: Default::default(),\n        token: Default::default(),\n      }),\n      events: vec![String::from(\"push\")],\n      name: String::from(\"web\"),\n    };\n    github_repos\n      .create_webhook(owner, repo, &request)\n      .await\n      .context(\"failed to create webhook\")?;\n\n    if !sync.config.webhook_enabled {\n      UpdateResourceSync {\n        id: sync.id,\n        config: PartialResourceSyncConfig {\n          webhook_enabled: Some(true),\n          ..Default::default()\n        },\n      }\n      .resolve(args)\n      .await\n      .map_err(|e| e.error)\n      .context(\"failed to update sync to enable webhook\")?;\n    }\n\n    Ok(NoData {})\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteSyncWebhook {\n  #[instrument(name = \"DeleteSyncWebhook\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<DeleteSyncWebhookResponse> {\n    let Some(github) = github_client() else {\n      return Err(\n        anyhow!(\n          \"github_webhook_app is not configured in core config toml\"\n        )\n        .into(),\n      );\n    };\n\n    let sync = get_check_permissions::<ResourceSync>(\n      &self.sync,\n      user,\n      PermissionLevel::Write.into(),\n    )\n    .await?;\n\n    if sync.config.git_provider != \"github.com\" {\n      return Err(\n        anyhow!(\"Can only manage github.com repo webhooks\").into(),\n      );\n    }\n\n    if sync.config.repo.is_empty() {\n      return Err(\n        anyhow!(\"No repo configured, can't create webhook\").into(),\n      );\n    }\n\n    let mut split = sync.config.repo.split('/');\n    let owner = split.next().context(\"Sync repo has no owner\")?;\n\n    let Some(github) = github.get(owner) else {\n      return Err(\n        anyhow!(\"Cannot manage repo webhooks under owner {owner}\")\n          .into(),\n      );\n    };\n\n    let repo =\n      split.next().context(\"Sync repo has no repo after the /\")?;\n\n    let github_repos = github.repos();\n\n    // First make sure the webhook isn't already created (inactive ones are ignored)\n    let webhooks = github_repos\n      .list_all_webhooks(owner, repo)\n      .await\n      .context(\"failed to list all webhooks on repo\")?\n      .body;\n\n    let CoreConfig {\n      host,\n      webhook_base_url,\n      ..\n    } = core_config();\n\n    let host = if webhook_base_url.is_empty() {\n      host\n    } else {\n      webhook_base_url\n    };\n    let url = match self.action {\n      SyncWebhookAction::Refresh => {\n        format!(\"{host}/listener/github/sync/{}/refresh\", sync.id)\n      }\n      SyncWebhookAction::Sync => {\n        format!(\"{host}/listener/github/sync/{}/sync\", sync.id)\n      }\n    };\n\n    for webhook in webhooks {\n      if webhook.active && webhook.config.url == url {\n        github_repos\n          .delete_webhook(owner, repo, webhook.id)\n          .await\n          .context(\"failed to delete webhook\")?;\n        return Ok(NoData {});\n      }\n    }\n\n    // No webhook to delete, all good\n    Ok(NoData {})\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/tag.rs",
    "content": "use std::str::FromStr;\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::{delete_one_by_id, update_one_by_id},\n  mongodb::bson::{doc, oid::ObjectId},\n};\nuse komodo_client::{\n  api::write::{CreateTag, DeleteTag, RenameTag, UpdateTagColor},\n  entities::{\n    action::Action, alerter::Alerter, build::Build, builder::Builder,\n    deployment::Deployment, procedure::Procedure, repo::Repo,\n    server::Server, stack::Stack, sync::ResourceSync, tag::Tag,\n  },\n};\nuse reqwest::StatusCode;\nuse resolver_api::Resolve;\nuse serror::AddStatusCodeError;\n\nuse crate::{\n  config::core_config,\n  helpers::query::{get_tag, get_tag_check_owner},\n  resource,\n  state::db_client,\n};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateTag {\n  #[instrument(name = \"CreateTag\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Tag> {\n    if core_config().disable_non_admin_create && !user.admin {\n      return Err(\n        anyhow!(\"Non admins cannot create tags\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    if ObjectId::from_str(&self.name).is_ok() {\n      return Err(\n        anyhow!(\"Tag name cannot be ObjectId\")\n          .status_code(StatusCode::BAD_REQUEST),\n      );\n    }\n\n    let mut tag = Tag {\n      id: Default::default(),\n      name: self.name,\n      color: self.color.unwrap_or_default(),\n      owner: user.id.clone(),\n    };\n\n    tag.id = db_client()\n      .tags\n      .insert_one(&tag)\n      .await\n      .context(\"failed to create tag on db\")?\n      .inserted_id\n      .as_object_id()\n      .context(\"inserted_id is not ObjectId\")?\n      .to_string();\n\n    Ok(tag)\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameTag {\n  #[instrument(name = \"RenameTag\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Tag> {\n    if ObjectId::from_str(&self.name).is_ok() {\n      return Err(anyhow!(\"tag name cannot be ObjectId\").into());\n    }\n\n    get_tag_check_owner(&self.id, user).await?;\n\n    update_one_by_id(\n      &db_client().tags,\n      &self.id,\n      doc! { \"$set\": { \"name\": self.name } },\n      None,\n    )\n    .await\n    .context(\"failed to rename tag on db\")?;\n\n    Ok(get_tag(&self.id).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateTagColor {\n  #[instrument(name = \"UpdateTagColor\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Tag> {\n    let tag = get_tag_check_owner(&self.tag, user).await?;\n\n    update_one_by_id(\n      &db_client().tags,\n      &tag.id,\n      doc! { \"$set\": { \"color\": self.color.as_ref() } },\n      None,\n    )\n    .await\n    .context(\"failed to rename tag on db\")?;\n\n    Ok(get_tag(&self.tag).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteTag {\n  #[instrument(name = \"DeleteTag\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<Tag> {\n    let tag = get_tag_check_owner(&self.id, user).await?;\n\n    tokio::try_join!(\n      resource::remove_tag_from_all::<Server>(&self.id),\n      resource::remove_tag_from_all::<Stack>(&self.id),\n      resource::remove_tag_from_all::<Deployment>(&self.id),\n      resource::remove_tag_from_all::<Build>(&self.id),\n      resource::remove_tag_from_all::<Repo>(&self.id),\n      resource::remove_tag_from_all::<Procedure>(&self.id),\n      resource::remove_tag_from_all::<Action>(&self.id),\n      resource::remove_tag_from_all::<ResourceSync>(&self.id),\n      resource::remove_tag_from_all::<Builder>(&self.id),\n      resource::remove_tag_from_all::<Alerter>(&self.id),\n    )?;\n\n    delete_one_by_id(&db_client().tags, &self.id, None).await?;\n\n    Ok(tag)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/user.rs",
    "content": "use std::str::FromStr;\n\nuse anyhow::{Context, anyhow};\nuse async_timing_util::unix_timestamp_ms;\nuse database::{\n  hash_password,\n  mungos::mongodb::bson::{doc, oid::ObjectId},\n};\nuse komodo_client::{\n  api::write::*,\n  entities::{\n    NoData,\n    user::{User, UserConfig},\n  },\n};\nuse reqwest::StatusCode;\nuse resolver_api::Resolve;\nuse serror::AddStatusCodeError;\n\nuse crate::{config::core_config, state::db_client};\n\nuse super::WriteArgs;\n\n//\n\nimpl Resolve<WriteArgs> for CreateLocalUser {\n  #[instrument(name = \"CreateLocalUser\", skip(admin, self), fields(admin_id = admin.id, username = self.username))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<CreateLocalUserResponse> {\n    if !admin.admin {\n      return Err(\n        anyhow!(\"This method is admin-only.\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    if self.username.is_empty() {\n      return Err(anyhow!(\"Username cannot be empty.\").into());\n    }\n\n    if ObjectId::from_str(&self.username).is_ok() {\n      return Err(\n        anyhow!(\"Username cannot be valid ObjectId\").into(),\n      );\n    }\n\n    if self.password.is_empty() {\n      return Err(anyhow!(\"Password cannot be empty.\").into());\n    }\n\n    let db = db_client();\n\n    if db\n      .users\n      .find_one(doc! { \"username\": &self.username })\n      .await\n      .context(\"Failed to query for existing users\")?\n      .is_some()\n    {\n      return Err(anyhow!(\"Username already taken.\").into());\n    }\n\n    let ts = unix_timestamp_ms() as i64;\n    let hashed_password = hash_password(self.password)?;\n\n    let mut user = User {\n      id: Default::default(),\n      username: self.username,\n      enabled: true,\n      admin: false,\n      super_admin: false,\n      create_server_permissions: false,\n      create_build_permissions: false,\n      updated_at: ts,\n      last_update_view: 0,\n      recents: Default::default(),\n      all: Default::default(),\n      config: UserConfig::Local {\n        password: hashed_password,\n      },\n    };\n\n    user.id = db_client()\n      .users\n      .insert_one(&user)\n      .await\n      .context(\"failed to create user\")?\n      .inserted_id\n      .as_object_id()\n      .context(\"inserted_id is not ObjectId\")?\n      .to_string();\n\n    user.sanitize();\n\n    Ok(user)\n  }\n}\n\n//\n\nimpl Resolve<WriteArgs> for UpdateUserUsername {\n  #[instrument(name = \"UpdateUserUsername\", skip(user), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<UpdateUserUsernameResponse> {\n    for locked_username in &core_config().lock_login_credentials_for {\n      if locked_username == \"__ALL__\"\n        || *locked_username == user.username\n      {\n        return Err(\n          anyhow!(\"User not allowed to update their username.\")\n            .into(),\n        );\n      }\n    }\n    if self.username.is_empty() {\n      return Err(anyhow!(\"Username cannot be empty.\").into());\n    }\n\n    if ObjectId::from_str(&self.username).is_ok() {\n      return Err(\n        anyhow!(\"Username cannot be valid ObjectId\").into(),\n      );\n    }\n\n    let db = db_client();\n    if db\n      .users\n      .find_one(doc! { \"username\": &self.username })\n      .await\n      .context(\"Failed to query for existing users\")?\n      .is_some()\n    {\n      return Err(anyhow!(\"Username already taken.\").into());\n    }\n    let id = ObjectId::from_str(&user.id)\n      .context(\"User id not valid ObjectId.\")?;\n    db.users\n      .update_one(\n        doc! { \"_id\": id },\n        doc! { \"$set\": { \"username\": self.username } },\n      )\n      .await\n      .context(\"Failed to update user username on database.\")?;\n    Ok(NoData {})\n  }\n}\n\n//\n\nimpl Resolve<WriteArgs> for UpdateUserPassword {\n  #[instrument(name = \"UpdateUserPassword\", skip(user, self), fields(user_id = user.id))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<UpdateUserPasswordResponse> {\n    for locked_username in &core_config().lock_login_credentials_for {\n      if locked_username == \"__ALL__\"\n        || *locked_username == user.username\n      {\n        return Err(\n          anyhow!(\"User not allowed to update their password.\")\n            .into(),\n        );\n      }\n    }\n    db_client().set_user_password(user, &self.password).await?;\n    Ok(NoData {})\n  }\n}\n\n//\n\nimpl Resolve<WriteArgs> for DeleteUser {\n  #[instrument(name = \"DeleteUser\", skip(admin), fields(user = self.user))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<DeleteUserResponse> {\n    if !admin.admin {\n      return Err(\n        anyhow!(\"This method is admin-only.\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n    if admin.username == self.user || admin.id == self.user {\n      return Err(anyhow!(\"User cannot delete themselves.\").into());\n    }\n    let query = if let Ok(id) = ObjectId::from_str(&self.user) {\n      doc! { \"_id\": id }\n    } else {\n      doc! { \"username\": self.user }\n    };\n    let db = db_client();\n    let Some(user) = db\n      .users\n      .find_one(query.clone())\n      .await\n      .context(\"Failed to query database for users.\")?\n    else {\n      return Err(\n        anyhow!(\"No user found with given id / username\").into(),\n      );\n    };\n    if user.super_admin {\n      return Err(\n        anyhow!(\"Cannot delete a super admin user.\").into(),\n      );\n    }\n    if user.admin && !admin.super_admin {\n      return Err(\n        anyhow!(\"Only a Super Admin can delete an admin user.\")\n          .into(),\n      );\n    }\n    db.users\n      .delete_one(query)\n      .await\n      .context(\"Failed to delete user from database\")?;\n    // Also remove user id from all user groups\n    if let Err(e) = db\n      .user_groups\n      .update_many(doc! {}, doc! { \"$pull\": { \"users\": &user.id } })\n      .await\n    {\n      warn!(\"Failed to remove deleted user from user groups | {e:?}\");\n    };\n    Ok(user)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/user_group.rs",
    "content": "use std::{collections::HashMap, str::FromStr};\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::{delete_one_by_id, find_one_by_id, update_one_by_id},\n  find::find_collect,\n  mongodb::bson::{doc, oid::ObjectId},\n};\nuse komodo_client::{\n  api::write::*,\n  entities::{komodo_timestamp, user_group::UserGroup},\n};\nuse reqwest::StatusCode;\nuse resolver_api::Resolve;\nuse serror::AddStatusCodeError;\n\nuse crate::state::db_client;\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateUserGroup {\n  #[instrument(name = \"CreateUserGroup\", skip(admin), fields(admin = admin.username))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<UserGroup> {\n    if !admin.admin {\n      return Err(\n        anyhow!(\"This call is admin-only\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n    let user_group = UserGroup {\n      name: self.name,\n      id: Default::default(),\n      everyone: Default::default(),\n      users: Default::default(),\n      all: Default::default(),\n      updated_at: komodo_timestamp(),\n    };\n    let db = db_client();\n    let id = db\n      .user_groups\n      .insert_one(user_group)\n      .await\n      .context(\"failed to create UserGroup on db\")?\n      .inserted_id\n      .as_object_id()\n      .context(\"inserted id is not ObjectId\")?\n      .to_string();\n    let res = find_one_by_id(&db.user_groups, &id)\n      .await\n      .context(\"failed to query db for user groups\")?\n      .context(\"user group at id not found\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<WriteArgs> for RenameUserGroup {\n  #[instrument(name = \"RenameUserGroup\", skip(admin), fields(admin = admin.username))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<UserGroup> {\n    if !admin.admin {\n      return Err(\n        anyhow!(\"This call is admin-only\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n    let db = db_client();\n    update_one_by_id(\n      &db.user_groups,\n      &self.id,\n      doc! { \"$set\": { \"name\": self.name } },\n      None,\n    )\n    .await\n    .context(\"failed to rename UserGroup on db\")?;\n    let res = find_one_by_id(&db.user_groups, &self.id)\n      .await\n      .context(\"failed to query db for UserGroups\")?\n      .context(\"no user group with given id\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteUserGroup {\n  #[instrument(name = \"DeleteUserGroup\", skip(admin), fields(admin = admin.username))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<UserGroup> {\n    if !admin.admin {\n      return Err(\n        anyhow!(\"This call is admin-only\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    let db = db_client();\n\n    let ug = find_one_by_id(&db.user_groups, &self.id)\n      .await\n      .context(\"failed to query db for UserGroups\")?\n      .context(\"no UserGroup found with given id\")?;\n\n    delete_one_by_id(&db.user_groups, &self.id, None)\n      .await\n      .context(\"failed to delete UserGroup from db\")?;\n\n    db.permissions\n      .delete_many(doc! {\n        \"user_target.type\": \"UserGroup\",\n        \"user_target.id\": self.id,\n      })\n      .await\n      .context(\"failed to clean up UserGroups permissions. User Group has been deleted\")?;\n\n    Ok(ug)\n  }\n}\n\nimpl Resolve<WriteArgs> for AddUserToUserGroup {\n  #[instrument(name = \"AddUserToUserGroup\", skip(admin), fields(admin = admin.username))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<UserGroup> {\n    if !admin.admin {\n      return Err(\n        anyhow!(\"This call is admin-only\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    let db = db_client();\n\n    let filter = match ObjectId::from_str(&self.user) {\n      Ok(id) => doc! { \"_id\": id },\n      Err(_) => doc! { \"username\": &self.user },\n    };\n    let user = db\n      .users\n      .find_one(filter)\n      .await\n      .context(\"failed to query mongo for users\")?\n      .context(\"no matching user found\")?;\n\n    let filter = match ObjectId::from_str(&self.user_group) {\n      Ok(id) => doc! { \"_id\": id },\n      Err(_) => doc! { \"name\": &self.user_group },\n    };\n    db.user_groups\n      .update_one(\n        filter.clone(),\n        doc! { \"$addToSet\": { \"users\": &user.id } },\n      )\n      .await\n      .context(\"failed to add user to group on db\")?;\n    let res = db\n      .user_groups\n      .find_one(filter)\n      .await\n      .context(\"failed to query db for UserGroups\")?\n      .context(\"no user group with given id\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<WriteArgs> for RemoveUserFromUserGroup {\n  #[instrument(name = \"RemoveUserFromUserGroup\", skip(admin), fields(admin = admin.username))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<UserGroup> {\n    if !admin.admin {\n      return Err(\n        anyhow!(\"This call is admin-only\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    let db = db_client();\n\n    let filter = match ObjectId::from_str(&self.user) {\n      Ok(id) => doc! { \"_id\": id },\n      Err(_) => doc! { \"username\": &self.user },\n    };\n    let user = db\n      .users\n      .find_one(filter)\n      .await\n      .context(\"failed to query mongo for users\")?\n      .context(\"no matching user found\")?;\n\n    let filter = match ObjectId::from_str(&self.user_group) {\n      Ok(id) => doc! { \"_id\": id },\n      Err(_) => doc! { \"name\": &self.user_group },\n    };\n    db.user_groups\n      .update_one(\n        filter.clone(),\n        doc! { \"$pull\": { \"users\": &user.id } },\n      )\n      .await\n      .context(\"failed to add user to group on db\")?;\n    let res = db\n      .user_groups\n      .find_one(filter)\n      .await\n      .context(\"failed to query db for UserGroups\")?\n      .context(\"no user group with given id\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<WriteArgs> for SetUsersInUserGroup {\n  #[instrument(name = \"SetUsersInUserGroup\", skip(admin), fields(admin = admin.username))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<UserGroup> {\n    if !admin.admin {\n      return Err(\n        anyhow!(\"This call is admin-only\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    let db = db_client();\n\n    let all_users = find_collect(&db.users, None, None)\n      .await\n      .context(\"failed to query db for users\")?\n      .into_iter()\n      .map(|u| (u.username, u.id))\n      .collect::<HashMap<_, _>>();\n\n    // Make sure all users are user ids\n    let users = self\n      .users\n      .into_iter()\n      .filter_map(|user| match ObjectId::from_str(&user) {\n        Ok(_) => Some(user),\n        Err(_) => all_users.get(&user).cloned(),\n      })\n      .collect::<Vec<_>>();\n\n    let filter = match ObjectId::from_str(&self.user_group) {\n      Ok(id) => doc! { \"_id\": id },\n      Err(_) => doc! { \"name\": &self.user_group },\n    };\n    db.user_groups\n      .update_one(filter.clone(), doc! { \"$set\": { \"users\": users } })\n      .await\n      .context(\"failed to set users on user group\")?;\n    let res = db\n      .user_groups\n      .find_one(filter)\n      .await\n      .context(\"failed to query db for UserGroups\")?\n      .context(\"no user group with given id\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<WriteArgs> for SetEveryoneUserGroup {\n  #[instrument(name = \"SetEveryoneUserGroup\", skip(admin), fields(admin = admin.username))]\n  async fn resolve(\n    self,\n    WriteArgs { user: admin }: &WriteArgs,\n  ) -> serror::Result<UserGroup> {\n    if !admin.admin {\n      return Err(\n        anyhow!(\"This call is admin-only\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    let db = db_client();\n\n    let filter = match ObjectId::from_str(&self.user_group) {\n      Ok(id) => doc! { \"_id\": id },\n      Err(_) => doc! { \"name\": &self.user_group },\n    };\n    db.user_groups\n      .update_one(\n        filter.clone(),\n        doc! { \"$set\": { \"everyone\": self.everyone } },\n      )\n      .await\n      .context(\"failed to set everyone on user group\")?;\n    let res = db\n      .user_groups\n      .find_one(filter)\n      .await\n      .context(\"failed to query db for UserGroups\")?\n      .context(\"no user group with given id\")?;\n    Ok(res)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/api/write/variable.rs",
    "content": "use anyhow::{Context, anyhow};\nuse database::mungos::mongodb::bson::doc;\nuse komodo_client::{\n  api::write::*,\n  entities::{Operation, ResourceTarget, variable::Variable},\n};\nuse reqwest::StatusCode;\nuse resolver_api::Resolve;\nuse serror::AddStatusCodeError;\n\nuse crate::{\n  helpers::{\n    query::get_variable,\n    update::{add_update, make_update},\n  },\n  state::db_client,\n};\n\nuse super::WriteArgs;\n\nimpl Resolve<WriteArgs> for CreateVariable {\n  #[instrument(name = \"CreateVariable\", skip(user, self), fields(name = &self.name))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<CreateVariableResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"Only admins can create variables\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    let CreateVariable {\n      name,\n      value,\n      description,\n      is_secret,\n    } = self;\n\n    let variable = Variable {\n      name,\n      value,\n      description,\n      is_secret,\n    };\n\n    db_client()\n      .variables\n      .insert_one(&variable)\n      .await\n      .context(\"Failed to create variable on db\")?;\n\n    let mut update = make_update(\n      ResourceTarget::system(),\n      Operation::CreateVariable,\n      user,\n    );\n\n    update\n      .push_simple_log(\"create variable\", format!(\"{variable:#?}\"));\n    update.finalize();\n\n    add_update(update).await?;\n\n    Ok(get_variable(&variable.name).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateVariableValue {\n  #[instrument(name = \"UpdateVariableValue\", skip(user, self), fields(name = &self.name))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<UpdateVariableValueResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"Only admins can update variables\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n\n    let UpdateVariableValue { name, value } = self;\n\n    let variable = get_variable(&name).await?;\n\n    if value == variable.value {\n      return Ok(variable);\n    }\n\n    db_client()\n      .variables\n      .update_one(\n        doc! { \"name\": &name },\n        doc! { \"$set\": { \"value\": &value } },\n      )\n      .await\n      .context(\"Failed to update variable value on db\")?;\n\n    let mut update = make_update(\n      ResourceTarget::system(),\n      Operation::UpdateVariableValue,\n      user,\n    );\n\n    let log = if variable.is_secret {\n      format!(\n        \"<span class=\\\"text-muted-foreground\\\">variable</span>: '{name}'\\n<span class=\\\"text-muted-foreground\\\">from</span>: <span class=\\\"text-red-500\\\">{}</span>\\n<span class=\\\"text-muted-foreground\\\">to</span>:   <span class=\\\"text-green-500\\\">{value}</span>\",\n        variable.value.replace(|_| true, \"#\")\n      )\n    } else {\n      format!(\n        \"<span class=\\\"text-muted-foreground\\\">variable</span>: '{name}'\\n<span class=\\\"text-muted-foreground\\\">from</span>: <span class=\\\"text-red-500\\\">{}</span>\\n<span class=\\\"text-muted-foreground\\\">to</span>:   <span class=\\\"text-green-500\\\">{value}</span>\",\n        variable.value\n      )\n    };\n\n    update.push_simple_log(\"Update Variable Value\", log);\n    update.finalize();\n\n    add_update(update).await?;\n\n    Ok(get_variable(&name).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateVariableDescription {\n  #[instrument(name = \"UpdateVariableDescription\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<UpdateVariableDescriptionResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"Only admins can update variables\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n    db_client()\n      .variables\n      .update_one(\n        doc! { \"name\": &self.name },\n        doc! { \"$set\": { \"description\": &self.description } },\n      )\n      .await\n      .context(\"Failed to update variable description on db\")?;\n    Ok(get_variable(&self.name).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for UpdateVariableIsSecret {\n  #[instrument(name = \"UpdateVariableIsSecret\", skip(user))]\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<UpdateVariableIsSecretResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"Only admins can update variables\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n    db_client()\n      .variables\n      .update_one(\n        doc! { \"name\": &self.name },\n        doc! { \"$set\": { \"is_secret\": self.is_secret } },\n      )\n      .await\n      .context(\"Failed to update variable is secret on db\")?;\n    Ok(get_variable(&self.name).await?)\n  }\n}\n\nimpl Resolve<WriteArgs> for DeleteVariable {\n  async fn resolve(\n    self,\n    WriteArgs { user }: &WriteArgs,\n  ) -> serror::Result<DeleteVariableResponse> {\n    if !user.admin {\n      return Err(\n        anyhow!(\"Only admins can delete variables\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n    let variable = get_variable(&self.name).await?;\n    db_client()\n      .variables\n      .delete_one(doc! { \"name\": &self.name })\n      .await\n      .context(\"Failed to delete variable on db\")?;\n\n    let mut update = make_update(\n      ResourceTarget::system(),\n      Operation::DeleteVariable,\n      user,\n    );\n\n    update\n      .push_simple_log(\"Delete Variable\", format!(\"{variable:#?}\"));\n    update.finalize();\n\n    add_update(update).await?;\n\n    Ok(variable)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/auth/github/client.rs",
    "content": "use std::sync::OnceLock;\n\nuse anyhow::{Context, anyhow};\nuse komodo_client::entities::config::core::{\n  CoreConfig, OauthCredentials,\n};\nuse reqwest::StatusCode;\nuse serde::{Deserialize, Serialize, de::DeserializeOwned};\nuse tokio::sync::Mutex;\n\nuse crate::{\n  auth::STATE_PREFIX_LENGTH, config::core_config,\n  helpers::random_string,\n};\n\npub fn github_oauth_client() -> &'static Option<GithubOauthClient> {\n  static GITHUB_OAUTH_CLIENT: OnceLock<Option<GithubOauthClient>> =\n    OnceLock::new();\n  GITHUB_OAUTH_CLIENT\n    .get_or_init(|| GithubOauthClient::new(core_config()))\n}\n\npub struct GithubOauthClient {\n  http: reqwest::Client,\n  client_id: String,\n  client_secret: String,\n  redirect_uri: String,\n  scopes: String,\n  states: Mutex<Vec<String>>,\n  user_agent: String,\n}\n\nimpl GithubOauthClient {\n  pub fn new(\n    CoreConfig {\n      github_oauth:\n        OauthCredentials {\n          enabled,\n          id,\n          secret,\n        },\n      host,\n      ..\n    }: &CoreConfig,\n  ) -> Option<GithubOauthClient> {\n    if !enabled {\n      return None;\n    }\n    if host.is_empty() {\n      warn!(\n        \"github oauth is enabled, but 'config.host' is not configured\"\n      );\n      return None;\n    }\n    if id.is_empty() {\n      warn!(\n        \"github oauth is enabled, but 'config.github_oauth.id' is not configured\"\n      );\n      return None;\n    }\n    if secret.is_empty() {\n      warn!(\n        \"github oauth is enabled, but 'config.github_oauth.secret' is not configured\"\n      );\n      return None;\n    }\n    GithubOauthClient {\n      http: reqwest::Client::new(),\n      client_id: id.clone(),\n      client_secret: secret.clone(),\n      redirect_uri: format!(\"{host}/auth/github/callback\"),\n      user_agent: Default::default(),\n      scopes: Default::default(),\n      states: Default::default(),\n    }\n    .into()\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn get_login_redirect_url(\n    &self,\n    redirect: Option<String>,\n  ) -> String {\n    let state_prefix = random_string(STATE_PREFIX_LENGTH);\n    let state = match redirect {\n      Some(redirect) => format!(\"{state_prefix}{redirect}\"),\n      None => state_prefix,\n    };\n    let redirect_url = format!(\n      \"https://github.com/login/oauth/authorize?state={state}&client_id={}&redirect_uri={}&scope={}\",\n      self.client_id, self.redirect_uri, self.scopes\n    );\n    let mut states = self.states.lock().await;\n    states.push(state);\n    redirect_url\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn check_state(&self, state: &str) -> bool {\n    let mut contained = false;\n    self.states.lock().await.retain(|s| {\n      if s.as_str() == state {\n        contained = true;\n        false\n      } else {\n        true\n      }\n    });\n    contained\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn get_access_token(\n    &self,\n    code: &str,\n  ) -> anyhow::Result<AccessTokenResponse> {\n    self\n      .post::<(), _>(\n        \"https://github.com/login/oauth/access_token\",\n        &[\n          (\"client_id\", self.client_id.as_str()),\n          (\"client_secret\", self.client_secret.as_str()),\n          (\"redirect_uri\", self.redirect_uri.as_str()),\n          (\"code\", code),\n        ],\n        None,\n        None,\n      )\n      .await\n      .context(\"failed to get github access token using code\")\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn get_github_user(\n    &self,\n    token: &str,\n  ) -> anyhow::Result<GithubUserResponse> {\n    self\n      .get(\"https://api.github.com/user\", &[], Some(token))\n      .await\n      .context(\"failed to get github user using access token\")\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  async fn get<R: DeserializeOwned>(\n    &self,\n    endpoint: &str,\n    query: &[(&str, &str)],\n    bearer_token: Option<&str>,\n  ) -> anyhow::Result<R> {\n    let mut req = self\n      .http\n      .get(endpoint)\n      .query(query)\n      .header(\"User-Agent\", &self.user_agent);\n\n    if let Some(bearer_token) = bearer_token {\n      req =\n        req.header(\"Authorization\", format!(\"Bearer {bearer_token}\"));\n    }\n\n    let res = req.send().await.context(\"failed to reach github\")?;\n\n    let status = res.status();\n\n    if status == StatusCode::OK {\n      let body = res\n        .json()\n        .await\n        .context(\"failed to parse body into expected type\")?;\n      Ok(body)\n    } else {\n      let text = res.text().await.context(format!(\n        \"status: {status} | failed to get response text\"\n      ))?;\n      Err(anyhow!(\"status: {status} | text: {text}\"))\n    }\n  }\n\n  async fn post<B: Serialize, R: DeserializeOwned>(\n    &self,\n    endpoint: &str,\n    query: &[(&str, &str)],\n    body: Option<&B>,\n    bearer_token: Option<&str>,\n  ) -> anyhow::Result<R> {\n    let mut req = self\n      .http\n      .post(endpoint)\n      .query(query)\n      .header(\"Accept\", \"application/json\")\n      .header(\"User-Agent\", &self.user_agent);\n\n    if let Some(body) = body {\n      req = req.json(body);\n    }\n\n    if let Some(bearer_token) = bearer_token {\n      req =\n        req.header(\"Authorization\", format!(\"Bearer {bearer_token}\"));\n    }\n\n    let res = req.send().await.context(\"failed to reach github\")?;\n\n    let status = res.status();\n\n    if status == StatusCode::OK {\n      let body = res\n        .json()\n        .await\n        .context(\"failed to parse POST body into expected type\")?;\n      Ok(body)\n    } else {\n      let text = res.text().await.with_context(|| format!(\n        \"method: POST | status: {status} | failed to get response text\"\n      ))?;\n      Err(anyhow!(\"method: POST | status: {status} | text: {text}\"))\n    }\n  }\n}\n\n#[derive(Deserialize)]\npub struct AccessTokenResponse {\n  pub access_token: String,\n  // pub scope: String,\n  // pub token_type: String,\n}\n\n#[derive(Deserialize)]\npub struct GithubUserResponse {\n  pub login: String,\n  pub id: u128,\n  pub avatar_url: String,\n  // pub email: Option<String>,\n}\n"
  },
  {
    "path": "bin/core/src/auth/github/mod.rs",
    "content": "use anyhow::{Context, anyhow};\nuse axum::{\n  Router, extract::Query, response::Redirect, routing::get,\n};\nuse database::mongo_indexed::Document;\nuse database::mungos::mongodb::bson::doc;\nuse komodo_client::entities::{\n  komodo_timestamp,\n  user::{User, UserConfig},\n};\nuse reqwest::StatusCode;\nuse serde::Deserialize;\nuse serror::AddStatusCode;\n\nuse crate::{\n  config::core_config,\n  helpers::random_string,\n  state::{db_client, jwt_client},\n};\n\nuse self::client::github_oauth_client;\n\nuse super::{RedirectQuery, STATE_PREFIX_LENGTH};\n\npub mod client;\n\npub fn router() -> Router {\n  Router::new()\n    .route(\n      \"/login\",\n      get(|Query(query): Query<RedirectQuery>| async {\n        Redirect::to(\n          &github_oauth_client()\n            .as_ref()\n            // OK: the router is only mounted in case that the client is populated\n            .unwrap()\n            .get_login_redirect_url(query.redirect)\n            .await,\n        )\n      }),\n    )\n    .route(\n      \"/callback\",\n      get(|query| async {\n        callback(query).await.status_code(StatusCode::UNAUTHORIZED)\n      }),\n    )\n}\n\n#[derive(Debug, Deserialize)]\nstruct CallbackQuery {\n  state: String,\n  code: String,\n}\n\n#[instrument(name = \"GithubCallback\", level = \"debug\")]\nasync fn callback(\n  Query(query): Query<CallbackQuery>,\n) -> anyhow::Result<Redirect> {\n  let client = github_oauth_client().as_ref().unwrap();\n  if !client.check_state(&query.state).await {\n    return Err(anyhow!(\"state mismatch\"));\n  }\n  let token = client.get_access_token(&query.code).await?;\n  let github_user =\n    client.get_github_user(&token.access_token).await?;\n  let github_id = github_user.id.to_string();\n  let db_client = db_client();\n  let user = db_client\n    .users\n    .find_one(doc! { \"config.data.github_id\": &github_id })\n    .await\n    .context(\"failed at find user query from database\")?;\n  let jwt = match user {\n    Some(user) => jwt_client()\n      .encode(user.id)\n      .context(\"failed to generate jwt\")?,\n    None => {\n      let ts = komodo_timestamp();\n      let no_users_exist =\n        db_client.users.find_one(Document::new()).await?.is_none();\n      let core_config = core_config();\n      if !no_users_exist && core_config.disable_user_registration {\n        return Err(anyhow!(\"User registration is disabled\"));\n      }\n\n      let mut username = github_user.login;\n      // Modify username if it already exists\n      if db_client\n        .users\n        .find_one(doc! { \"username\": &username })\n        .await\n        .context(\"Failed to query users collection\")?\n        .is_some()\n      {\n        username += \"-\";\n        username += &random_string(5);\n      };\n\n      let user = User {\n        id: Default::default(),\n        username,\n        enabled: no_users_exist || core_config.enable_new_users,\n        admin: no_users_exist,\n        super_admin: no_users_exist,\n        create_server_permissions: no_users_exist,\n        create_build_permissions: no_users_exist,\n        updated_at: ts,\n        last_update_view: 0,\n        recents: Default::default(),\n        all: Default::default(),\n        config: UserConfig::Github {\n          github_id,\n          avatar: github_user.avatar_url,\n        },\n      };\n      let user_id = db_client\n        .users\n        .insert_one(user)\n        .await\n        .context(\"failed to create user on mongo\")?\n        .inserted_id\n        .as_object_id()\n        .context(\"inserted_id is not ObjectId\")?\n        .to_string();\n      jwt_client()\n        .encode(user_id)\n        .context(\"failed to generate jwt\")?\n    }\n  };\n  let exchange_token = jwt_client().create_exchange_token(jwt).await;\n  let redirect = &query.state[STATE_PREFIX_LENGTH..];\n  let redirect_url = if redirect.is_empty() {\n    format!(\"{}?token={exchange_token}\", core_config().host)\n  } else {\n    let splitter = if redirect.contains('?') { '&' } else { '?' };\n    format!(\"{redirect}{splitter}token={exchange_token}\")\n  };\n  Ok(Redirect::to(&redirect_url))\n}\n"
  },
  {
    "path": "bin/core/src/auth/google/client.rs",
    "content": "use std::sync::OnceLock;\n\nuse anyhow::{Context, anyhow};\nuse jsonwebtoken::{DecodingKey, Validation, decode};\nuse komodo_client::entities::config::core::{\n  CoreConfig, OauthCredentials,\n};\nuse reqwest::StatusCode;\nuse serde::{Deserialize, de::DeserializeOwned};\nuse tokio::sync::Mutex;\n\nuse crate::{\n  auth::STATE_PREFIX_LENGTH, config::core_config,\n  helpers::random_string,\n};\n\npub fn google_oauth_client() -> &'static Option<GoogleOauthClient> {\n  static GOOGLE_OAUTH_CLIENT: OnceLock<Option<GoogleOauthClient>> =\n    OnceLock::new();\n  GOOGLE_OAUTH_CLIENT\n    .get_or_init(|| GoogleOauthClient::new(core_config()))\n}\n\npub struct GoogleOauthClient {\n  http: reqwest::Client,\n  client_id: String,\n  client_secret: String,\n  redirect_uri: String,\n  scopes: String,\n  states: Mutex<Vec<String>>,\n  user_agent: String,\n}\n\nimpl GoogleOauthClient {\n  pub fn new(\n    CoreConfig {\n      google_oauth:\n        OauthCredentials {\n          enabled,\n          id,\n          secret,\n        },\n      host,\n      ..\n    }: &CoreConfig,\n  ) -> Option<GoogleOauthClient> {\n    if !enabled {\n      return None;\n    }\n    if host.is_empty() {\n      warn!(\n        \"google oauth is enabled, but 'config.host' is not configured\"\n      );\n      return None;\n    }\n    if id.is_empty() {\n      warn!(\n        \"google oauth is enabled, but 'config.google_oauth.id' is not configured\"\n      );\n      return None;\n    }\n    if secret.is_empty() {\n      warn!(\n        \"google oauth is enabled, but 'config.google_oauth.secret' is not configured\"\n      );\n      return None;\n    }\n    let scopes = urlencoding::encode(\n      &[\n        \"https://www.googleapis.com/auth/userinfo.profile\",\n        \"https://www.googleapis.com/auth/userinfo.email\",\n      ]\n      .join(\" \"),\n    )\n    .to_string();\n    GoogleOauthClient {\n      http: Default::default(),\n      client_id: id.clone(),\n      client_secret: secret.clone(),\n      redirect_uri: format!(\"{host}/auth/google/callback\"),\n      user_agent: String::from(\"komodo\"),\n      states: Default::default(),\n      scopes,\n    }\n    .into()\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn get_login_redirect_url(\n    &self,\n    redirect: Option<String>,\n  ) -> String {\n    let state_prefix = random_string(STATE_PREFIX_LENGTH);\n    let state = match redirect {\n      Some(redirect) => format!(\"{state_prefix}{redirect}\"),\n      None => state_prefix,\n    };\n    let redirect_url = format!(\n      \"https://accounts.google.com/o/oauth2/v2/auth?response_type=code&state={state}&client_id={}&redirect_uri={}&scope={}\",\n      self.client_id, self.redirect_uri, self.scopes\n    );\n    let mut states = self.states.lock().await;\n    states.push(state);\n    redirect_url\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn check_state(&self, state: &str) -> bool {\n    let mut contained = false;\n    self.states.lock().await.retain(|s| {\n      if s.as_str() == state {\n        contained = true;\n        false\n      } else {\n        true\n      }\n    });\n    contained\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn get_access_token(\n    &self,\n    code: &str,\n  ) -> anyhow::Result<AccessTokenResponse> {\n    self\n      .post::<_>(\n        \"https://oauth2.googleapis.com/token\",\n        &[\n          (\"client_id\", self.client_id.as_str()),\n          (\"client_secret\", self.client_secret.as_str()),\n          (\"redirect_uri\", self.redirect_uri.as_str()),\n          (\"code\", code),\n          (\"grant_type\", \"authorization_code\"),\n        ],\n        None,\n      )\n      .await\n      .context(\"failed to get google access token using code\")\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub fn get_google_user(\n    &self,\n    id_token: &str,\n  ) -> anyhow::Result<GoogleUser> {\n    let mut v = Validation::new(Default::default());\n    v.insecure_disable_signature_validation();\n    v.validate_aud = false;\n    let res = decode::<GoogleUser>(\n      id_token,\n      &DecodingKey::from_secret(b\"\"),\n      &v,\n    )\n    .context(\"failed to decode google id token\")?;\n    Ok(res.claims)\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  async fn post<R: DeserializeOwned>(\n    &self,\n    endpoint: &str,\n    body: &[(&str, &str)],\n    bearer_token: Option<&str>,\n  ) -> anyhow::Result<R> {\n    let mut req = self\n      .http\n      .post(endpoint)\n      .form(body)\n      .header(\"Accept\", \"application/json\")\n      .header(\"User-Agent\", &self.user_agent);\n\n    if let Some(bearer_token) = bearer_token {\n      req =\n        req.header(\"Authorization\", format!(\"Bearer {bearer_token}\"));\n    }\n\n    let res = req.send().await.context(\"failed to reach google\")?;\n\n    let status = res.status();\n\n    if status == StatusCode::OK {\n      let body = res\n        .json()\n        .await\n        .context(\"failed to parse POST body into expected type\")?;\n      Ok(body)\n    } else {\n      let text = res.text().await.context(format!(\n        \"method: POST | status: {status} | failed to get response text\"\n      ))?;\n      Err(anyhow!(\"method: POST | status: {status} | text: {text}\"))\n    }\n  }\n}\n\n#[derive(Deserialize)]\npub struct AccessTokenResponse {\n  // pub access_token: String,\n  pub id_token: String,\n  // pub scope: String,\n  // pub token_type: String,\n}\n\n#[derive(Deserialize, Clone)]\npub struct GoogleUser {\n  #[serde(rename = \"sub\")]\n  pub id: String,\n  pub email: String,\n  #[serde(default)]\n  pub picture: String,\n}\n"
  },
  {
    "path": "bin/core/src/auth/google/mod.rs",
    "content": "use anyhow::{Context, anyhow};\nuse async_timing_util::unix_timestamp_ms;\nuse axum::{\n  Router, extract::Query, response::Redirect, routing::get,\n};\nuse database::mongo_indexed::Document;\nuse database::mungos::mongodb::bson::doc;\nuse komodo_client::entities::user::{User, UserConfig};\nuse reqwest::StatusCode;\nuse serde::Deserialize;\nuse serror::AddStatusCode;\n\nuse crate::{\n  config::core_config,\n  helpers::random_string,\n  state::{db_client, jwt_client},\n};\n\nuse self::client::google_oauth_client;\n\nuse super::{RedirectQuery, STATE_PREFIX_LENGTH};\n\npub mod client;\n\npub fn router() -> Router {\n  Router::new()\n    .route(\n      \"/login\",\n      get(|Query(query): Query<RedirectQuery>| async move {\n        Redirect::to(\n          &google_oauth_client()\n            .as_ref()\n            // OK: its not mounted unless the client is populated\n            .unwrap()\n            .get_login_redirect_url(query.redirect)\n            .await,\n        )\n      }),\n    )\n    .route(\n      \"/callback\",\n      get(|query| async {\n        callback(query).await.status_code(StatusCode::UNAUTHORIZED)\n      }),\n    )\n}\n\n#[derive(Debug, Deserialize)]\nstruct CallbackQuery {\n  state: Option<String>,\n  code: Option<String>,\n  error: Option<String>,\n}\n\n#[instrument(name = \"GoogleCallback\", level = \"debug\")]\nasync fn callback(\n  Query(query): Query<CallbackQuery>,\n) -> anyhow::Result<Redirect> {\n  // Safe: the method is only called after the client is_some\n  let client = google_oauth_client().as_ref().unwrap();\n  if let Some(error) = query.error {\n    return Err(anyhow!(\"auth error from google: {error}\"));\n  }\n  let state = query\n    .state\n    .context(\"callback query does not contain state\")?;\n  if !client.check_state(&state).await {\n    return Err(anyhow!(\"state mismatch\"));\n  }\n  let token = client\n    .get_access_token(\n      &query.code.context(\"callback query does not contain code\")?,\n    )\n    .await?;\n  let google_user = client.get_google_user(&token.id_token)?;\n  let google_id = google_user.id.to_string();\n  let db_client = db_client();\n  let user = db_client\n    .users\n    .find_one(doc! { \"config.data.google_id\": &google_id })\n    .await\n    .context(\"failed at find user query from mongo\")?;\n  let jwt = match user {\n    Some(user) => jwt_client()\n      .encode(user.id)\n      .context(\"failed to generate jwt\")?,\n    None => {\n      let ts = unix_timestamp_ms() as i64;\n      let no_users_exist =\n        db_client.users.find_one(Document::new()).await?.is_none();\n      let core_config = core_config();\n      if !no_users_exist && core_config.disable_user_registration {\n        return Err(anyhow!(\"User registration is disabled\"));\n      }\n      let mut username = google_user\n        .email\n        .split('@')\n        .collect::<Vec<&str>>()\n        .first()\n        .unwrap()\n        .to_string();\n      // Modify username if it already exists\n      if db_client\n        .users\n        .find_one(doc! { \"username\": &username })\n        .await\n        .context(\"Failed to query users collection\")?\n        .is_some()\n      {\n        username += \"-\";\n        username += &random_string(5);\n      };\n\n      let user = User {\n        id: Default::default(),\n        username,\n        enabled: no_users_exist || core_config.enable_new_users,\n        admin: no_users_exist,\n        super_admin: no_users_exist,\n        create_server_permissions: no_users_exist,\n        create_build_permissions: no_users_exist,\n        updated_at: ts,\n        last_update_view: 0,\n        recents: Default::default(),\n        all: Default::default(),\n        config: UserConfig::Google {\n          google_id,\n          avatar: google_user.picture,\n        },\n      };\n      let user_id = db_client\n        .users\n        .insert_one(user)\n        .await\n        .context(\"failed to create user on mongo\")?\n        .inserted_id\n        .as_object_id()\n        .context(\"inserted_id is not ObjectId\")?\n        .to_string();\n      jwt_client()\n        .encode(user_id)\n        .context(\"failed to generate jwt\")?\n    }\n  };\n  let exchange_token = jwt_client().create_exchange_token(jwt).await;\n  let redirect = &state[STATE_PREFIX_LENGTH..];\n  let redirect_url = if redirect.is_empty() {\n    format!(\"{}?token={exchange_token}\", core_config().host)\n  } else {\n    let splitter = if redirect.contains('?') { '&' } else { '?' };\n    format!(\"{redirect}{splitter}token={exchange_token}\")\n  };\n  Ok(Redirect::to(&redirect_url))\n}\n"
  },
  {
    "path": "bin/core/src/auth/jwt.rs",
    "content": "use std::collections::HashMap;\n\nuse anyhow::{Context, anyhow};\nuse async_timing_util::{\n  Timelength, get_timelength_in_ms, unix_timestamp_ms,\n};\nuse database::mungos::mongodb::bson::doc;\nuse jsonwebtoken::{\n  DecodingKey, EncodingKey, Header, Validation, decode, encode,\n};\nuse komodo_client::{\n  api::auth::JwtResponse, entities::config::core::CoreConfig,\n};\nuse serde::{Deserialize, Serialize};\nuse tokio::sync::Mutex;\n\nuse crate::helpers::random_string;\n\ntype ExchangeTokenMap = Mutex<HashMap<String, (JwtResponse, u128)>>;\n\n#[derive(Serialize, Deserialize)]\npub struct JwtClaims {\n  pub id: String,\n  pub iat: u128,\n  pub exp: u128,\n}\n\npub struct JwtClient {\n  header: Header,\n  validation: Validation,\n  encoding_key: EncodingKey,\n  decoding_key: DecodingKey,\n  ttl_ms: u128,\n  exchange_tokens: ExchangeTokenMap,\n}\n\nimpl JwtClient {\n  pub fn new(config: &CoreConfig) -> anyhow::Result<JwtClient> {\n    let secret = if config.jwt_secret.is_empty() {\n      random_string(40)\n    } else {\n      config.jwt_secret.clone()\n    };\n    Ok(JwtClient {\n      header: Header::default(),\n      validation: Validation::new(Default::default()),\n      encoding_key: EncodingKey::from_secret(secret.as_bytes()),\n      decoding_key: DecodingKey::from_secret(secret.as_bytes()),\n      ttl_ms: get_timelength_in_ms(\n        config.jwt_ttl.to_string().parse()?,\n      ),\n      exchange_tokens: Default::default(),\n    })\n  }\n\n  pub fn encode(\n    &self,\n    user_id: String,\n  ) -> anyhow::Result<JwtResponse> {\n    let iat = unix_timestamp_ms();\n    let exp = iat + self.ttl_ms;\n    let claims = JwtClaims {\n      id: user_id.clone(),\n      iat,\n      exp,\n    };\n    let jwt = encode(&self.header, &claims, &self.encoding_key)\n      .context(\"failed at signing claim\")?;\n    Ok(JwtResponse { user_id, jwt })\n  }\n\n  pub fn decode(&self, jwt: &str) -> anyhow::Result<JwtClaims> {\n    decode::<JwtClaims>(jwt, &self.decoding_key, &self.validation)\n      .map(|res| res.claims)\n      .context(\"failed to decode token claims\")\n  }\n\n  #[instrument(level = \"debug\", skip_all)]\n  pub async fn create_exchange_token(\n    &self,\n    jwt: JwtResponse,\n  ) -> String {\n    let exchange_token = random_string(40);\n    self.exchange_tokens.lock().await.insert(\n      exchange_token.clone(),\n      (\n        jwt,\n        unix_timestamp_ms()\n          + get_timelength_in_ms(Timelength::OneMinute),\n      ),\n    );\n    exchange_token\n  }\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn redeem_exchange_token(\n    &self,\n    exchange_token: &str,\n  ) -> anyhow::Result<JwtResponse> {\n    let (jwt, valid_until) = self\n      .exchange_tokens\n      .lock()\n      .await\n      .remove(exchange_token)\n      .context(\"invalid exchange token: unrecognized\")?;\n    if unix_timestamp_ms() < valid_until {\n      Ok(jwt)\n    } else {\n      Err(anyhow!(\"invalid exchange token: expired\"))\n    }\n  }\n}\n"
  },
  {
    "path": "bin/core/src/auth/local.rs",
    "content": "use std::str::FromStr;\n\nuse anyhow::{Context, anyhow};\nuse async_timing_util::unix_timestamp_ms;\nuse database::{\n  hash_password,\n  mungos::mongodb::bson::{Document, doc, oid::ObjectId},\n};\nuse komodo_client::{\n  api::auth::{\n    LoginLocalUser, LoginLocalUserResponse, SignUpLocalUser,\n    SignUpLocalUserResponse,\n  },\n  entities::user::{User, UserConfig},\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  api::auth::AuthArgs,\n  config::core_config,\n  state::{db_client, jwt_client},\n};\n\nimpl Resolve<AuthArgs> for SignUpLocalUser {\n  #[instrument(name = \"SignUpLocalUser\", skip(self))]\n  async fn resolve(\n    self,\n    _: &AuthArgs,\n  ) -> serror::Result<SignUpLocalUserResponse> {\n    let core_config = core_config();\n\n    if !core_config.local_auth {\n      return Err(anyhow!(\"Local auth is not enabled\").into());\n    }\n\n    if self.username.is_empty() {\n      return Err(anyhow!(\"Username cannot be empty string\").into());\n    }\n\n    if ObjectId::from_str(&self.username).is_ok() {\n      return Err(\n        anyhow!(\"Username cannot be valid ObjectId\").into(),\n      );\n    }\n\n    if self.password.is_empty() {\n      return Err(anyhow!(\"Password cannot be empty string\").into());\n    }\n\n    let db = db_client();\n\n    let no_users_exist =\n      db.users.find_one(Document::new()).await?.is_none();\n\n    if !no_users_exist && core_config.disable_user_registration {\n      return Err(anyhow!(\"User registration is disabled\").into());\n    }\n\n    if db\n      .users\n      .find_one(doc! { \"username\": &self.username })\n      .await\n      .context(\"Failed to query for existing users\")?\n      .is_some()\n    {\n      return Err(anyhow!(\"Username already taken.\").into());\n    }\n\n    let ts = unix_timestamp_ms() as i64;\n    let hashed_password = hash_password(self.password)?;\n\n    let user = User {\n      id: Default::default(),\n      username: self.username,\n      enabled: no_users_exist || core_config.enable_new_users,\n      admin: no_users_exist,\n      super_admin: no_users_exist,\n      create_server_permissions: no_users_exist,\n      create_build_permissions: no_users_exist,\n      updated_at: ts,\n      last_update_view: 0,\n      recents: Default::default(),\n      all: Default::default(),\n      config: UserConfig::Local {\n        password: hashed_password,\n      },\n    };\n\n    let user_id = db_client()\n      .users\n      .insert_one(user)\n      .await\n      .context(\"failed to create user\")?\n      .inserted_id\n      .as_object_id()\n      .context(\"inserted_id is not ObjectId\")?\n      .to_string();\n\n    jwt_client()\n      .encode(user_id.clone())\n      .context(\"failed to generate jwt for user\")\n      .map_err(Into::into)\n  }\n}\n\nimpl Resolve<AuthArgs> for LoginLocalUser {\n  #[instrument(name = \"LoginLocalUser\", level = \"debug\", skip(self))]\n  async fn resolve(\n    self,\n    _: &AuthArgs,\n  ) -> serror::Result<LoginLocalUserResponse> {\n    if !core_config().local_auth {\n      return Err(anyhow!(\"local auth is not enabled\").into());\n    }\n\n    let user = db_client()\n      .users\n      .find_one(doc! { \"username\": &self.username })\n      .await\n      .context(\"failed at db query for users\")?\n      .with_context(|| {\n        format!(\"did not find user with username {}\", self.username)\n      })?;\n\n    let UserConfig::Local {\n      password: user_pw_hash,\n    } = user.config\n    else {\n      return Err(\n        anyhow!(\n          \"non-local auth users can not log in with a password\"\n        )\n        .into(),\n      );\n    };\n\n    let verified = bcrypt::verify(self.password, &user_pw_hash)\n      .context(\"failed at verify password\")?;\n\n    if !verified {\n      return Err(anyhow!(\"invalid credentials\").into());\n    }\n\n    jwt_client()\n      .encode(user.id.clone())\n      .context(\"failed at generating jwt for user\")\n      .map_err(Into::into)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/auth/mod.rs",
    "content": "use anyhow::{Context, anyhow};\nuse async_timing_util::unix_timestamp_ms;\nuse axum::{\n  extract::Request, http::HeaderMap, middleware::Next,\n  response::Response,\n};\nuse database::mungos::mongodb::bson::doc;\nuse komodo_client::entities::{komodo_timestamp, user::User};\nuse reqwest::StatusCode;\nuse serde::Deserialize;\nuse serror::AddStatusCode;\n\nuse crate::{\n  helpers::query::get_user,\n  state::{db_client, jwt_client},\n};\n\nuse self::jwt::JwtClaims;\n\npub mod github;\npub mod google;\npub mod jwt;\npub mod oidc;\n\nmod local;\n\nconst STATE_PREFIX_LENGTH: usize = 20;\n\n#[derive(Debug, Deserialize)]\nstruct RedirectQuery {\n  redirect: Option<String>,\n}\n\n#[instrument(level = \"debug\")]\npub async fn auth_request(\n  headers: HeaderMap,\n  mut req: Request,\n  next: Next,\n) -> serror::Result<Response> {\n  let user = authenticate_check_enabled(&headers)\n    .await\n    .status_code(StatusCode::UNAUTHORIZED)?;\n  req.extensions_mut().insert(user);\n  Ok(next.run(req).await)\n}\n\n#[instrument(level = \"debug\")]\npub async fn get_user_id_from_headers(\n  headers: &HeaderMap,\n) -> anyhow::Result<String> {\n  match (\n    headers.get(\"authorization\"),\n    headers.get(\"x-api-key\"),\n    headers.get(\"x-api-secret\"),\n  ) {\n    (Some(jwt), _, _) => {\n      // USE JWT\n      let jwt = jwt.to_str().context(\"jwt is not str\")?;\n      auth_jwt_get_user_id(jwt)\n        .await\n        .context(\"failed to authenticate jwt\")\n    }\n    (None, Some(key), Some(secret)) => {\n      // USE API KEY / SECRET\n      let key = key.to_str().context(\"key is not str\")?;\n      let secret = secret.to_str().context(\"secret is not str\")?;\n      auth_api_key_get_user_id(key, secret)\n        .await\n        .context(\"failed to authenticate api key\")\n    }\n    _ => {\n      // AUTH FAIL\n      Err(anyhow!(\n        \"must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET\"\n      ))\n    }\n  }\n}\n\n#[instrument(level = \"debug\")]\npub async fn authenticate_check_enabled(\n  headers: &HeaderMap,\n) -> anyhow::Result<User> {\n  let user_id = get_user_id_from_headers(headers).await?;\n  let user = get_user(&user_id).await?;\n  if user.enabled {\n    Ok(user)\n  } else {\n    Err(anyhow!(\"user not enabled\"))\n  }\n}\n\n#[instrument(level = \"debug\")]\npub async fn auth_jwt_get_user_id(\n  jwt: &str,\n) -> anyhow::Result<String> {\n  let claims: JwtClaims = jwt_client().decode(jwt)?;\n  if claims.exp > unix_timestamp_ms() {\n    Ok(claims.id)\n  } else {\n    Err(anyhow!(\"token has expired\"))\n  }\n}\n\n#[instrument(level = \"debug\")]\npub async fn auth_jwt_check_enabled(\n  jwt: &str,\n) -> anyhow::Result<User> {\n  let user_id = auth_jwt_get_user_id(jwt).await?;\n  check_enabled(user_id).await\n}\n\n#[instrument(level = \"debug\")]\npub async fn auth_api_key_get_user_id(\n  key: &str,\n  secret: &str,\n) -> anyhow::Result<String> {\n  let key = db_client()\n    .api_keys\n    .find_one(doc! { \"key\": key })\n    .await\n    .context(\"failed to query db\")?\n    .context(\"no api key matching key\")?;\n  if key.expires != 0 && key.expires < komodo_timestamp() {\n    return Err(anyhow!(\"api key expired\"));\n  }\n  if bcrypt::verify(secret, &key.secret)\n    .context(\"failed to verify secret hash\")?\n  {\n    // secret matches\n    Ok(key.user_id)\n  } else {\n    // secret mismatch\n    Err(anyhow!(\"invalid api secret\"))\n  }\n}\n\n#[instrument(level = \"debug\")]\npub async fn auth_api_key_check_enabled(\n  key: &str,\n  secret: &str,\n) -> anyhow::Result<User> {\n  let user_id = auth_api_key_get_user_id(key, secret).await?;\n  check_enabled(user_id).await\n}\n\n#[instrument(level = \"debug\")]\nasync fn check_enabled(user_id: String) -> anyhow::Result<User> {\n  let user = get_user(&user_id).await?;\n  if user.enabled {\n    Ok(user)\n  } else {\n    Err(anyhow!(\"user not enabled\"))\n  }\n}\n"
  },
  {
    "path": "bin/core/src/auth/oidc/client.rs",
    "content": "use std::{sync::OnceLock, time::Duration};\n\nuse anyhow::Context;\nuse arc_swap::ArcSwapOption;\nuse openidconnect::{\n  Client, ClientId, ClientSecret, EmptyAdditionalClaims,\n  EndpointMaybeSet, EndpointNotSet, EndpointSet, IssuerUrl,\n  RedirectUrl, StandardErrorResponse, core::*,\n};\n\nuse crate::config::core_config;\n\ntype OidcClient = Client<\n  EmptyAdditionalClaims,\n  CoreAuthDisplay,\n  CoreGenderClaim,\n  CoreJweContentEncryptionAlgorithm,\n  CoreJsonWebKey,\n  CoreAuthPrompt,\n  StandardErrorResponse<CoreErrorResponseType>,\n  CoreTokenResponse,\n  CoreTokenIntrospectionResponse,\n  CoreRevocableToken,\n  CoreRevocationErrorResponse,\n  EndpointSet,\n  EndpointNotSet,\n  EndpointNotSet,\n  EndpointNotSet,\n  EndpointMaybeSet,\n  EndpointMaybeSet,\n>;\n\npub fn oidc_client() -> &'static ArcSwapOption<OidcClient> {\n  static OIDC_CLIENT: OnceLock<ArcSwapOption<OidcClient>> =\n    OnceLock::new();\n  OIDC_CLIENT.get_or_init(Default::default)\n}\n\n/// The OIDC client must be reinitialized to\n/// pick up the latest provider JWKs. This\n/// function spawns a management thread to do this\n/// on a loop.\npub async fn spawn_oidc_client_management() {\n  let config = core_config();\n  if !config.oidc_enabled\n    || config.oidc_provider.is_empty()\n    || config.oidc_client_id.is_empty()\n  {\n    return;\n  }\n  if let Err(e) = reset_oidc_client().await {\n    error!(\"Failed to initialize OIDC client | {e:#}\");\n  }\n  tokio::spawn(async move {\n    loop {\n      tokio::time::sleep(Duration::from_secs(60)).await;\n      if let Err(e) = reset_oidc_client().await {\n        warn!(\"Failed to reinitialize OIDC client | {e:#}\");\n      }\n    }\n  });\n}\n\nasync fn reset_oidc_client() -> anyhow::Result<()> {\n  let config = core_config();\n  // Use OpenID Connect Discovery to fetch the provider metadata.\n  let provider_metadata = CoreProviderMetadata::discover_async(\n    IssuerUrl::new(config.oidc_provider.clone())?,\n    super::reqwest_client(),\n  )\n  .await\n  .context(\"Failed to get OIDC /.well-known/openid-configuration\")?;\n\n  let client = CoreClient::from_provider_metadata(\n    provider_metadata,\n    ClientId::new(config.oidc_client_id.to_string()),\n    // The secret may be empty / ommitted if auth provider supports PKCE\n    if config.oidc_client_secret.is_empty() {\n      None\n    } else {\n      Some(ClientSecret::new(config.oidc_client_secret.to_string()))\n    },\n  )\n  // Set the URL the user will be redirected to after the authorization process.\n  .set_redirect_uri(RedirectUrl::new(format!(\n    \"{}/auth/oidc/callback\",\n    core_config().host\n  ))?);\n\n  oidc_client().store(Some(client.into()));\n\n  Ok(())\n}\n"
  },
  {
    "path": "bin/core/src/auth/oidc/mod.rs",
    "content": "use std::sync::OnceLock;\n\nuse anyhow::{Context, anyhow};\nuse axum::{\n  Router, extract::Query, response::Redirect, routing::get,\n};\nuse client::oidc_client;\nuse dashmap::DashMap;\nuse database::mungos::mongodb::bson::{Document, doc};\nuse komodo_client::entities::{\n  komodo_timestamp,\n  user::{User, UserConfig},\n};\nuse openidconnect::{\n  AccessTokenHash, AuthorizationCode, CsrfToken,\n  EmptyAdditionalClaims, Nonce, OAuth2TokenResponse,\n  PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse,\n  core::{CoreAuthenticationFlow, CoreGenderClaim},\n};\nuse reqwest::StatusCode;\nuse serde::Deserialize;\nuse serror::AddStatusCode;\n\nuse crate::{\n  config::core_config,\n  helpers::random_string,\n  state::{db_client, jwt_client},\n};\n\nuse super::RedirectQuery;\n\npub mod client;\n\nstatic APP_USER_AGENT: &str =\n  concat!(\"Komodo/\", env!(\"CARGO_PKG_VERSION\"),);\n\nfn reqwest_client() -> &'static reqwest::Client {\n  static REQWEST: OnceLock<reqwest::Client> = OnceLock::new();\n  REQWEST.get_or_init(|| {\n    reqwest::Client::builder()\n      .redirect(reqwest::redirect::Policy::none())\n      .user_agent(APP_USER_AGENT)\n      .build()\n      .expect(\"Invalid OIDC reqwest client\")\n  })\n}\n\n/// CSRF tokens can only be used once from the callback,\n/// and must be used within this timeframe\nconst CSRF_VALID_FOR_MS: i64 = 120_000; // 2 minutes for user to log in.\n\ntype RedirectUrl = Option<String>;\n/// Maps the csrf secrets to other information added in the \"login\" method (before auth provider redirect).\n/// This information is retrieved in the \"callback\" method (after auth provider redirect).\ntype VerifierMap =\n  DashMap<String, (PkceCodeVerifier, Nonce, RedirectUrl, i64)>;\nfn verifier_tokens() -> &'static VerifierMap {\n  static VERIFIERS: OnceLock<VerifierMap> = OnceLock::new();\n  VERIFIERS.get_or_init(Default::default)\n}\n\npub fn router() -> Router {\n  Router::new()\n    .route(\n      \"/login\",\n      get(|query| async {\n        login(query).await.status_code(StatusCode::UNAUTHORIZED)\n      }),\n    )\n    .route(\n      \"/callback\",\n      get(|query| async {\n        callback(query).await.status_code(StatusCode::UNAUTHORIZED)\n      }),\n    )\n}\n\n#[instrument(name = \"OidcRedirect\", level = \"debug\")]\nasync fn login(\n  Query(RedirectQuery { redirect }): Query<RedirectQuery>,\n) -> anyhow::Result<Redirect> {\n  let client = oidc_client().load();\n  let client =\n    client.as_ref().context(\"OIDC Client not configured\")?;\n\n  let (pkce_challenge, pkce_verifier) =\n    PkceCodeChallenge::new_random_sha256();\n\n  // Generate the authorization URL.\n  let (auth_url, csrf_token, nonce) = client\n    .authorize_url(\n      CoreAuthenticationFlow::AuthorizationCode,\n      CsrfToken::new_random,\n      Nonce::new_random,\n    )\n    .set_pkce_challenge(pkce_challenge)\n    .add_scope(Scope::new(\"openid\".to_string()))\n    .add_scope(Scope::new(\"profile\".to_string()))\n    .add_scope(Scope::new(\"email\".to_string()))\n    .url();\n\n  // Data inserted here will be matched on callback side for csrf protection.\n  verifier_tokens().insert(\n    csrf_token.secret().clone(),\n    (\n      pkce_verifier,\n      nonce,\n      redirect,\n      komodo_timestamp() + CSRF_VALID_FOR_MS,\n    ),\n  );\n\n  let config = core_config();\n  let redirect = if !config.oidc_redirect_host.is_empty() {\n    let auth_url = auth_url.as_str();\n    let (protocol, rest) = auth_url\n      .split_once(\"://\")\n      .context(\"Invalid URL: Missing protocol (eg 'https://')\")?;\n    let host = rest\n      .split_once(['/', '?'])\n      .map(|(host, _)| host)\n      .unwrap_or(rest);\n    Redirect::to(&auth_url.replace(\n      &format!(\"{protocol}://{host}\"),\n      &config.oidc_redirect_host,\n    ))\n  } else {\n    Redirect::to(auth_url.as_str())\n  };\n\n  Ok(redirect)\n}\n\n#[derive(Debug, Deserialize)]\nstruct CallbackQuery {\n  state: Option<String>,\n  code: Option<String>,\n  error: Option<String>,\n}\n\n#[instrument(name = \"OidcCallback\", level = \"debug\")]\nasync fn callback(\n  Query(query): Query<CallbackQuery>,\n) -> anyhow::Result<Redirect> {\n  let client = oidc_client().load();\n  let client =\n    client.as_ref().context(\"OIDC Client not initialized successfully. Is the provider properly configured?\")?;\n\n  if let Some(e) = query.error {\n    return Err(anyhow!(\"Provider returned error: {e}\"));\n  }\n\n  let code = query.code.context(\"Provider did not return code\")?;\n  let state = CsrfToken::new(\n    query.state.context(\"Provider did not return state\")?,\n  );\n\n  let (_, (pkce_verifier, nonce, redirect, valid_until)) =\n    verifier_tokens()\n      .remove(state.secret())\n      .context(\"CSRF token invalid\")?;\n\n  if komodo_timestamp() > valid_until {\n    return Err(anyhow!(\n      \"CSRF token invalid (Timed out). The token must be used within 2 minutes.\"\n    ));\n  }\n\n  let reqwest_client = reqwest_client();\n  let token_response = client\n    .exchange_code(AuthorizationCode::new(code))\n    .context(\"Failed to get Oauth token at exchange code\")?\n    .set_pkce_verifier(pkce_verifier)\n    .request_async(reqwest_client)\n    .await\n    .context(\"Failed to get Oauth token\")?;\n\n  // Extract the ID token claims after verifying its authenticity and nonce.\n  let id_token = token_response\n    .id_token()\n    .context(\"OIDC Server did not return an ID token\")?;\n\n  // Some providers attach additional audiences, they must be added here\n  // so token verification succeeds.\n  let verifier = client.id_token_verifier();\n  let additional_audiences = &core_config().oidc_additional_audiences;\n  let verifier = if additional_audiences.is_empty() {\n    verifier\n  } else {\n    verifier.set_other_audience_verifier_fn(|aud| {\n      additional_audiences.contains(aud)\n    })\n  };\n\n  let claims = id_token\n    .claims(&verifier, &nonce)\n    .context(\"Failed to verify token claims. This issue may be temporary (60 seconds max).\")?;\n\n  // Verify the access token hash to ensure that the access token hasn't been substituted for\n  // another user's.\n  if let Some(expected_access_token_hash) = claims.access_token_hash()\n  {\n    let actual_access_token_hash = AccessTokenHash::from_token(\n      token_response.access_token(),\n      id_token.signing_alg()?,\n      id_token.signing_key(&verifier)?,\n    )?;\n    if actual_access_token_hash != *expected_access_token_hash {\n      return Err(anyhow!(\"Invalid access token\"));\n    }\n  }\n\n  let user_id = claims.subject().as_str();\n\n  let db_client = db_client();\n  let user = db_client\n    .users\n    .find_one(doc! {\n      \"config.data.provider\": &core_config().oidc_provider,\n      \"config.data.user_id\": user_id\n    })\n    .await\n    .context(\"failed at find user query from database\")?;\n\n  let jwt = match user {\n    Some(user) => jwt_client()\n      .encode(user.id)\n      .context(\"failed to generate jwt\")?,\n    None => {\n      let ts = komodo_timestamp();\n      let no_users_exist =\n        db_client.users.find_one(Document::new()).await?.is_none();\n      let core_config = core_config();\n      if !no_users_exist && core_config.disable_user_registration {\n        return Err(anyhow!(\"User registration is disabled\"));\n      }\n\n      // Fetch user info\n      let user_info = client\n        .user_info(\n          token_response.access_token().clone(),\n          claims.subject().clone().into(),\n        )\n        .context(\"Invalid user info request\")?\n        .request_async::<EmptyAdditionalClaims, _, CoreGenderClaim>(\n          reqwest_client,\n        )\n        .await\n        .context(\"Failed to fetch user info for new user\")?;\n\n      // Will use preferred_username, then email, then user_id if it isn't available.\n      let mut username = user_info\n        .preferred_username()\n        .map(|username| username.to_string())\n        .unwrap_or_else(|| {\n          let email = user_info\n            .email()\n            .map(|email| email.as_str())\n            .unwrap_or(user_id);\n          if core_config.oidc_use_full_email {\n            email\n          } else {\n            email\n              .split_once('@')\n              .map(|(username, _)| username)\n              .unwrap_or(email)\n          }\n          .to_string()\n        });\n\n      // Modify username if it already exists\n      if db_client\n        .users\n        .find_one(doc! { \"username\": &username })\n        .await\n        .context(\"Failed to query users collection\")?\n        .is_some()\n      {\n        username += \"-\";\n        username += &random_string(5);\n      };\n\n      let user = User {\n        id: Default::default(),\n        username,\n        enabled: no_users_exist || core_config.enable_new_users,\n        admin: no_users_exist,\n        super_admin: no_users_exist,\n        create_server_permissions: no_users_exist,\n        create_build_permissions: no_users_exist,\n        updated_at: ts,\n        last_update_view: 0,\n        recents: Default::default(),\n        all: Default::default(),\n        config: UserConfig::Oidc {\n          provider: core_config.oidc_provider.clone(),\n          user_id: user_id.to_string(),\n        },\n      };\n\n      let user_id = db_client\n        .users\n        .insert_one(user)\n        .await\n        .context(\"failed to create user on database\")?\n        .inserted_id\n        .as_object_id()\n        .context(\"inserted_id is not ObjectId\")?\n        .to_string();\n\n      jwt_client()\n        .encode(user_id)\n        .context(\"failed to generate jwt\")?\n    }\n  };\n  let exchange_token = jwt_client().create_exchange_token(jwt).await;\n  let redirect_url = if let Some(redirect) = redirect {\n    let splitter = if redirect.contains('?') { '&' } else { '?' };\n    format!(\"{redirect}{splitter}token={exchange_token}\")\n  } else {\n    format!(\"{}?token={exchange_token}\", core_config().host)\n  };\n  Ok(Redirect::to(&redirect_url))\n}\n"
  },
  {
    "path": "bin/core/src/cloud/aws/ec2.rs",
    "content": "use std::time::Duration;\n\nuse anyhow::{Context, anyhow};\nuse aws_config::{BehaviorVersion, Region};\nuse aws_sdk_ec2::{\n  Client,\n  types::{\n    BlockDeviceMapping, EbsBlockDevice,\n    InstanceNetworkInterfaceSpecification, InstanceStateChange,\n    InstanceStateName, InstanceStatus, InstanceType, ResourceType,\n    Tag, TagSpecification,\n  },\n};\nuse base64::Engine;\nuse komodo_client::entities::{\n  ResourceTarget,\n  alert::{Alert, AlertData, SeverityLevel},\n  builder::AwsBuilderConfig,\n  komodo_timestamp,\n};\n\nuse crate::{alert::send_alerts, config::core_config};\n\nconst POLL_RATE_SECS: u64 = 2;\nconst MAX_POLL_TRIES: usize = 30;\n\npub struct Ec2Instance {\n  pub instance_id: String,\n  pub ip: String,\n}\n\n/// Provides credentials in the core config file to the AWS client\n#[derive(Debug)]\nstruct CredentialsFromConfig;\n\nimpl aws_credential_types::provider::ProvideCredentials\n  for CredentialsFromConfig\n{\n  fn provide_credentials<'a>(\n    &'a self,\n  ) -> aws_credential_types::provider::future::ProvideCredentials<'a>\n  where\n    Self: 'a,\n  {\n    aws_credential_types::provider::future::ProvideCredentials::new(\n      async {\n        let config = core_config();\n        Ok(aws_credential_types::Credentials::new(\n          &config.aws.access_key_id,\n          &config.aws.secret_access_key,\n          None,\n          None,\n          \"komodo-config\",\n        ))\n      },\n    )\n  }\n}\n\n#[instrument]\nasync fn create_ec2_client(region: String) -> Client {\n  let region = Region::new(region);\n  let config = aws_config::defaults(BehaviorVersion::latest())\n    .region(region)\n    .credentials_provider(CredentialsFromConfig)\n    .load()\n    .await;\n  Client::new(&config)\n}\n\n#[instrument]\npub async fn launch_ec2_instance(\n  name: &str,\n  config: &AwsBuilderConfig,\n) -> anyhow::Result<Ec2Instance> {\n  let AwsBuilderConfig {\n    region,\n    instance_type,\n    volume_gb,\n    ami_id,\n    subnet_id,\n    security_group_ids,\n    key_pair_name,\n    assign_public_ip,\n    use_public_ip,\n    user_data,\n    port: _,\n    use_https: _,\n    git_providers: _,\n    docker_registries: _,\n    secrets: _,\n  } = config;\n  let instance_type = handle_unknown_instance_type(\n    InstanceType::from(instance_type.as_str()),\n  )?;\n  let client = create_ec2_client(region.clone()).await;\n  let req = client\n    .run_instances()\n    .image_id(ami_id)\n    .instance_type(instance_type)\n    .network_interfaces(\n      InstanceNetworkInterfaceSpecification::builder()\n        .subnet_id(subnet_id)\n        .associate_public_ip_address(*assign_public_ip)\n        .set_groups(security_group_ids.to_vec().into())\n        .device_index(0)\n        .build(),\n    )\n    .key_name(key_pair_name)\n    .tag_specifications(\n      TagSpecification::builder()\n        .tags(Tag::builder().key(\"Name\").value(name).build())\n        .resource_type(ResourceType::Instance)\n        .build(),\n    )\n    .block_device_mappings(\n      BlockDeviceMapping::builder()\n        .set_device_name(\"/dev/sda1\".to_string().into())\n        .set_ebs(\n          EbsBlockDevice::builder()\n            .volume_size(*volume_gb)\n            .build()\n            .into(),\n        )\n        .build(),\n    )\n    .min_count(1)\n    .max_count(1)\n    .user_data(\n      base64::engine::general_purpose::STANDARD_NO_PAD\n        .encode(user_data),\n    );\n\n  let res = req\n    .send()\n    .await\n    .context(\"failed to start builder ec2 instance\")?;\n\n  let instance = res\n    .instances()\n    .first()\n    .context(\"instances array is empty\")?;\n\n  let instance_id = instance\n    .instance_id()\n    .context(\"instance does not have instance_id\")?\n    .to_string();\n\n  for _ in 0..MAX_POLL_TRIES {\n    let state_name =\n      get_ec2_instance_state_name(&client, &instance_id).await?;\n    if state_name == Some(InstanceStateName::Running) {\n      let ip = if *use_public_ip {\n        get_ec2_instance_public_ip(&client, &instance_id).await?\n      } else {\n        instance\n          .private_ip_address()\n          .ok_or(anyhow!(\"instance does not have private ip\"))?\n          .to_string()\n      };\n      return Ok(Ec2Instance { instance_id, ip });\n    }\n    tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;\n  }\n  Err(anyhow!(\"instance not running after polling\"))\n}\n\nconst MAX_TERMINATION_TRIES: usize = 5;\nconst TERMINATION_WAIT_SECS: u64 = 15;\n\n#[instrument]\npub async fn terminate_ec2_instance_with_retry(\n  region: String,\n  instance_id: &str,\n) -> anyhow::Result<InstanceStateChange> {\n  let client = create_ec2_client(region).await;\n  for i in 0..MAX_TERMINATION_TRIES {\n    match terminate_ec2_instance_inner(&client, instance_id).await {\n      Ok(res) => {\n        info!(\"instance {instance_id} successfully terminated.\");\n        return Ok(res);\n      }\n      Err(e) => {\n        if i == MAX_TERMINATION_TRIES - 1 {\n          error!(\"failed to terminate aws instance {instance_id}.\");\n          let alert = Alert {\n            id: Default::default(),\n            ts: komodo_timestamp(),\n            resolved: false,\n            level: SeverityLevel::Critical,\n            target: ResourceTarget::system(),\n            data: AlertData::AwsBuilderTerminationFailed {\n              instance_id: instance_id.to_string(),\n              message: format!(\"{e:#}\"),\n            },\n            resolved_ts: None,\n          };\n          send_alerts(&[alert]).await;\n          return Err(e);\n        }\n        tokio::time::sleep(Duration::from_secs(\n          TERMINATION_WAIT_SECS,\n        ))\n        .await;\n      }\n    }\n  }\n  unreachable!()\n}\n\n#[instrument(skip(client))]\nasync fn terminate_ec2_instance_inner(\n  client: &Client,\n  instance_id: &str,\n) -> anyhow::Result<InstanceStateChange> {\n  let res = client\n    .terminate_instances()\n    .instance_ids(instance_id)\n    .send()\n    .await\n    .context(\"failed to terminate instance from aws\")?\n    .terminating_instances()\n    .first()\n    .context(\"terminating instances is empty\")?\n    .to_owned();\n  Ok(res)\n}\n\n/// Automatically retries 5 times, waiting 2 sec in between\n#[instrument(level = \"debug\")]\nasync fn get_ec2_instance_status(\n  client: &Client,\n  instance_id: &str,\n) -> anyhow::Result<Option<InstanceStatus>> {\n  let mut try_count = 1;\n  loop {\n    match async {\n      anyhow::Ok(\n        client\n          .describe_instance_status()\n          .instance_ids(instance_id)\n          .send()\n          .await\n          .context(\"failed to describe instance status from aws\")?\n          .instance_statuses()\n          .first()\n          .cloned(),\n      )\n    }\n    .await\n    {\n      Ok(res) => return Ok(res),\n      Err(e) if try_count > 4 => return Err(e),\n      Err(_) => {\n        tokio::time::sleep(Duration::from_secs(2)).await;\n        try_count += 1;\n      }\n    }\n  }\n}\n\n#[instrument(level = \"debug\")]\nasync fn get_ec2_instance_state_name(\n  client: &Client,\n  instance_id: &str,\n) -> anyhow::Result<Option<InstanceStateName>> {\n  let status = get_ec2_instance_status(client, instance_id).await?;\n  if status.is_none() {\n    return Ok(None);\n  }\n  let state = status\n    .unwrap()\n    .instance_state()\n    .ok_or(anyhow!(\"instance state is None\"))?\n    .name()\n    .ok_or(anyhow!(\"instance state name is None\"))?\n    .to_owned();\n  Ok(Some(state))\n}\n\n/// Automatically retries 5 times, waiting 2 sec in between\n#[instrument(level = \"debug\")]\nasync fn get_ec2_instance_public_ip(\n  client: &Client,\n  instance_id: &str,\n) -> anyhow::Result<String> {\n  let mut try_count = 1;\n  loop {\n    match async {\n      anyhow::Ok(\n        client\n          .describe_instances()\n          .instance_ids(instance_id)\n          .send()\n          .await\n          .context(\"failed to describe instances from aws\")?\n          .reservations()\n          .first()\n          .context(\"instance reservations is empty\")?\n          .instances()\n          .first()\n          .context(\"instances is empty\")?\n          .public_ip_address()\n          .context(\"instance has no public ip\")?\n          .to_string(),\n      )\n    }\n    .await\n    {\n      Ok(res) => return Ok(res),\n      Err(e) if try_count > 4 => return Err(e),\n      Err(_) => {\n        tokio::time::sleep(Duration::from_secs(2)).await;\n        try_count += 1;\n      }\n    }\n  }\n}\n\nfn handle_unknown_instance_type(\n  instance_type: InstanceType,\n) -> anyhow::Result<InstanceType> {\n  match instance_type {\n    InstanceType::A12xlarge\n    | InstanceType::A14xlarge\n    | InstanceType::A1Large\n    | InstanceType::A1Medium\n    | InstanceType::A1Metal\n    | InstanceType::A1Xlarge\n    | InstanceType::C1Medium\n    | InstanceType::C1Xlarge\n    | InstanceType::C32xlarge\n    | InstanceType::C34xlarge\n    | InstanceType::C38xlarge\n    | InstanceType::C3Large\n    | InstanceType::C3Xlarge\n    | InstanceType::C42xlarge\n    | InstanceType::C44xlarge\n    | InstanceType::C48xlarge\n    | InstanceType::C4Large\n    | InstanceType::C4Xlarge\n    | InstanceType::C512xlarge\n    | InstanceType::C518xlarge\n    | InstanceType::C524xlarge\n    | InstanceType::C52xlarge\n    | InstanceType::C54xlarge\n    | InstanceType::C59xlarge\n    | InstanceType::C5Large\n    | InstanceType::C5Metal\n    | InstanceType::C5Xlarge\n    | InstanceType::C5a12xlarge\n    | InstanceType::C5a16xlarge\n    | InstanceType::C5a24xlarge\n    | InstanceType::C5a2xlarge\n    | InstanceType::C5a4xlarge\n    | InstanceType::C5a8xlarge\n    | InstanceType::C5aLarge\n    | InstanceType::C5aXlarge\n    | InstanceType::C5ad12xlarge\n    | InstanceType::C5ad16xlarge\n    | InstanceType::C5ad24xlarge\n    | InstanceType::C5ad2xlarge\n    | InstanceType::C5ad4xlarge\n    | InstanceType::C5ad8xlarge\n    | InstanceType::C5adLarge\n    | InstanceType::C5adXlarge\n    | InstanceType::C5d12xlarge\n    | InstanceType::C5d18xlarge\n    | InstanceType::C5d24xlarge\n    | InstanceType::C5d2xlarge\n    | InstanceType::C5d4xlarge\n    | InstanceType::C5d9xlarge\n    | InstanceType::C5dLarge\n    | InstanceType::C5dMetal\n    | InstanceType::C5dXlarge\n    | InstanceType::C5n18xlarge\n    | InstanceType::C5n2xlarge\n    | InstanceType::C5n4xlarge\n    | InstanceType::C5n9xlarge\n    | InstanceType::C5nLarge\n    | InstanceType::C5nMetal\n    | InstanceType::C5nXlarge\n    | InstanceType::C6a12xlarge\n    | InstanceType::C6a16xlarge\n    | InstanceType::C6a24xlarge\n    | InstanceType::C6a2xlarge\n    | InstanceType::C6a32xlarge\n    | InstanceType::C6a48xlarge\n    | InstanceType::C6a4xlarge\n    | InstanceType::C6a8xlarge\n    | InstanceType::C6aLarge\n    | InstanceType::C6aMetal\n    | InstanceType::C6aXlarge\n    | InstanceType::C6g12xlarge\n    | InstanceType::C6g16xlarge\n    | InstanceType::C6g2xlarge\n    | InstanceType::C6g4xlarge\n    | InstanceType::C6g8xlarge\n    | InstanceType::C6gLarge\n    | InstanceType::C6gMedium\n    | InstanceType::C6gMetal\n    | InstanceType::C6gXlarge\n    | InstanceType::C6gd12xlarge\n    | InstanceType::C6gd16xlarge\n    | InstanceType::C6gd2xlarge\n    | InstanceType::C6gd4xlarge\n    | InstanceType::C6gd8xlarge\n    | InstanceType::C6gdLarge\n    | InstanceType::C6gdMedium\n    | InstanceType::C6gdMetal\n    | InstanceType::C6gdXlarge\n    | InstanceType::C6gn12xlarge\n    | InstanceType::C6gn16xlarge\n    | InstanceType::C6gn2xlarge\n    | InstanceType::C6gn4xlarge\n    | InstanceType::C6gn8xlarge\n    | InstanceType::C6gnLarge\n    | InstanceType::C6gnMedium\n    | InstanceType::C6gnXlarge\n    | InstanceType::C6i12xlarge\n    | InstanceType::C6i16xlarge\n    | InstanceType::C6i24xlarge\n    | InstanceType::C6i2xlarge\n    | InstanceType::C6i32xlarge\n    | InstanceType::C6i4xlarge\n    | InstanceType::C6i8xlarge\n    | InstanceType::C6iLarge\n    | InstanceType::C6iMetal\n    | InstanceType::C6iXlarge\n    | InstanceType::C6id12xlarge\n    | InstanceType::C6id16xlarge\n    | InstanceType::C6id24xlarge\n    | InstanceType::C6id2xlarge\n    | InstanceType::C6id32xlarge\n    | InstanceType::C6id4xlarge\n    | InstanceType::C6id8xlarge\n    | InstanceType::C6idLarge\n    | InstanceType::C6idMetal\n    | InstanceType::C6idXlarge\n    | InstanceType::C6in12xlarge\n    | InstanceType::C6in16xlarge\n    | InstanceType::C6in24xlarge\n    | InstanceType::C6in2xlarge\n    | InstanceType::C6in32xlarge\n    | InstanceType::C6in4xlarge\n    | InstanceType::C6in8xlarge\n    | InstanceType::C6inLarge\n    | InstanceType::C6inMetal\n    | InstanceType::C6inXlarge\n    | InstanceType::C7a12xlarge\n    | InstanceType::C7a16xlarge\n    | InstanceType::C7a24xlarge\n    | InstanceType::C7a2xlarge\n    | InstanceType::C7a32xlarge\n    | InstanceType::C7a48xlarge\n    | InstanceType::C7a4xlarge\n    | InstanceType::C7a8xlarge\n    | InstanceType::C7aLarge\n    | InstanceType::C7aMedium\n    | InstanceType::C7aMetal48xl\n    | InstanceType::C7aXlarge\n    | InstanceType::C7g12xlarge\n    | InstanceType::C7g16xlarge\n    | InstanceType::C7g2xlarge\n    | InstanceType::C7g4xlarge\n    | InstanceType::C7g8xlarge\n    | InstanceType::C7gLarge\n    | InstanceType::C7gMedium\n    | InstanceType::C7gMetal\n    | InstanceType::C7gXlarge\n    | InstanceType::C7gd12xlarge\n    | InstanceType::C7gd16xlarge\n    | InstanceType::C7gd2xlarge\n    | InstanceType::C7gd4xlarge\n    | InstanceType::C7gd8xlarge\n    | InstanceType::C7gdLarge\n    | InstanceType::C7gdMedium\n    | InstanceType::C7gdXlarge\n    | InstanceType::C7gn12xlarge\n    | InstanceType::C7gn16xlarge\n    | InstanceType::C7gn2xlarge\n    | InstanceType::C7gn4xlarge\n    | InstanceType::C7gn8xlarge\n    | InstanceType::C7gnLarge\n    | InstanceType::C7gnMedium\n    | InstanceType::C7gnXlarge\n    | InstanceType::C7i12xlarge\n    | InstanceType::C7i16xlarge\n    | InstanceType::C7i24xlarge\n    | InstanceType::C7i2xlarge\n    | InstanceType::C7i48xlarge\n    | InstanceType::C7i4xlarge\n    | InstanceType::C7i8xlarge\n    | InstanceType::C7iLarge\n    | InstanceType::C7iMetal24xl\n    | InstanceType::C7iMetal48xl\n    | InstanceType::C7iXlarge\n    | InstanceType::Cc14xlarge\n    | InstanceType::Cc28xlarge\n    | InstanceType::Cg14xlarge\n    | InstanceType::Cr18xlarge\n    | InstanceType::D22xlarge\n    | InstanceType::D24xlarge\n    | InstanceType::D28xlarge\n    | InstanceType::D2Xlarge\n    | InstanceType::D32xlarge\n    | InstanceType::D34xlarge\n    | InstanceType::D38xlarge\n    | InstanceType::D3Xlarge\n    | InstanceType::D3en12xlarge\n    | InstanceType::D3en2xlarge\n    | InstanceType::D3en4xlarge\n    | InstanceType::D3en6xlarge\n    | InstanceType::D3en8xlarge\n    | InstanceType::D3enXlarge\n    | InstanceType::Dl124xlarge\n    | InstanceType::Dl2q24xlarge\n    | InstanceType::F116xlarge\n    | InstanceType::F12xlarge\n    | InstanceType::F14xlarge\n    | InstanceType::G22xlarge\n    | InstanceType::G28xlarge\n    | InstanceType::G316xlarge\n    | InstanceType::G34xlarge\n    | InstanceType::G38xlarge\n    | InstanceType::G3sXlarge\n    | InstanceType::G4ad16xlarge\n    | InstanceType::G4ad2xlarge\n    | InstanceType::G4ad4xlarge\n    | InstanceType::G4ad8xlarge\n    | InstanceType::G4adXlarge\n    | InstanceType::G4dn12xlarge\n    | InstanceType::G4dn16xlarge\n    | InstanceType::G4dn2xlarge\n    | InstanceType::G4dn4xlarge\n    | InstanceType::G4dn8xlarge\n    | InstanceType::G4dnMetal\n    | InstanceType::G4dnXlarge\n    | InstanceType::G512xlarge\n    | InstanceType::G516xlarge\n    | InstanceType::G524xlarge\n    | InstanceType::G52xlarge\n    | InstanceType::G548xlarge\n    | InstanceType::G54xlarge\n    | InstanceType::G58xlarge\n    | InstanceType::G5Xlarge\n    | InstanceType::G5g16xlarge\n    | InstanceType::G5g2xlarge\n    | InstanceType::G5g4xlarge\n    | InstanceType::G5g8xlarge\n    | InstanceType::G5gMetal\n    | InstanceType::G5gXlarge\n    | InstanceType::H116xlarge\n    | InstanceType::H12xlarge\n    | InstanceType::H14xlarge\n    | InstanceType::H18xlarge\n    | InstanceType::Hi14xlarge\n    | InstanceType::Hpc6a48xlarge\n    | InstanceType::Hpc6id32xlarge\n    | InstanceType::Hpc7a12xlarge\n    | InstanceType::Hpc7a24xlarge\n    | InstanceType::Hpc7a48xlarge\n    | InstanceType::Hpc7a96xlarge\n    | InstanceType::Hpc7g16xlarge\n    | InstanceType::Hpc7g4xlarge\n    | InstanceType::Hpc7g8xlarge\n    | InstanceType::Hs18xlarge\n    | InstanceType::I22xlarge\n    | InstanceType::I24xlarge\n    | InstanceType::I28xlarge\n    | InstanceType::I2Xlarge\n    | InstanceType::I316xlarge\n    | InstanceType::I32xlarge\n    | InstanceType::I34xlarge\n    | InstanceType::I38xlarge\n    | InstanceType::I3Large\n    | InstanceType::I3Metal\n    | InstanceType::I3Xlarge\n    | InstanceType::I3en12xlarge\n    | InstanceType::I3en24xlarge\n    | InstanceType::I3en2xlarge\n    | InstanceType::I3en3xlarge\n    | InstanceType::I3en6xlarge\n    | InstanceType::I3enLarge\n    | InstanceType::I3enMetal\n    | InstanceType::I3enXlarge\n    | InstanceType::I4g16xlarge\n    | InstanceType::I4g2xlarge\n    | InstanceType::I4g4xlarge\n    | InstanceType::I4g8xlarge\n    | InstanceType::I4gLarge\n    | InstanceType::I4gXlarge\n    | InstanceType::I4i12xlarge\n    | InstanceType::I4i16xlarge\n    | InstanceType::I4i24xlarge\n    | InstanceType::I4i2xlarge\n    | InstanceType::I4i32xlarge\n    | InstanceType::I4i4xlarge\n    | InstanceType::I4i8xlarge\n    | InstanceType::I4iLarge\n    | InstanceType::I4iMetal\n    | InstanceType::I4iXlarge\n    | InstanceType::Im4gn16xlarge\n    | InstanceType::Im4gn2xlarge\n    | InstanceType::Im4gn4xlarge\n    | InstanceType::Im4gn8xlarge\n    | InstanceType::Im4gnLarge\n    | InstanceType::Im4gnXlarge\n    | InstanceType::Inf124xlarge\n    | InstanceType::Inf12xlarge\n    | InstanceType::Inf16xlarge\n    | InstanceType::Inf1Xlarge\n    | InstanceType::Inf224xlarge\n    | InstanceType::Inf248xlarge\n    | InstanceType::Inf28xlarge\n    | InstanceType::Inf2Xlarge\n    | InstanceType::Is4gen2xlarge\n    | InstanceType::Is4gen4xlarge\n    | InstanceType::Is4gen8xlarge\n    | InstanceType::Is4genLarge\n    | InstanceType::Is4genMedium\n    | InstanceType::Is4genXlarge\n    | InstanceType::M1Large\n    | InstanceType::M1Medium\n    | InstanceType::M1Small\n    | InstanceType::M1Xlarge\n    | InstanceType::M22xlarge\n    | InstanceType::M24xlarge\n    | InstanceType::M2Xlarge\n    | InstanceType::M32xlarge\n    | InstanceType::M3Large\n    | InstanceType::M3Medium\n    | InstanceType::M3Xlarge\n    | InstanceType::M410xlarge\n    | InstanceType::M416xlarge\n    | InstanceType::M42xlarge\n    | InstanceType::M44xlarge\n    | InstanceType::M4Large\n    | InstanceType::M4Xlarge\n    | InstanceType::M512xlarge\n    | InstanceType::M516xlarge\n    | InstanceType::M524xlarge\n    | InstanceType::M52xlarge\n    | InstanceType::M54xlarge\n    | InstanceType::M58xlarge\n    | InstanceType::M5Large\n    | InstanceType::M5Metal\n    | InstanceType::M5Xlarge\n    | InstanceType::M5a12xlarge\n    | InstanceType::M5a16xlarge\n    | InstanceType::M5a24xlarge\n    | InstanceType::M5a2xlarge\n    | InstanceType::M5a4xlarge\n    | InstanceType::M5a8xlarge\n    | InstanceType::M5aLarge\n    | InstanceType::M5aXlarge\n    | InstanceType::M5ad12xlarge\n    | InstanceType::M5ad16xlarge\n    | InstanceType::M5ad24xlarge\n    | InstanceType::M5ad2xlarge\n    | InstanceType::M5ad4xlarge\n    | InstanceType::M5ad8xlarge\n    | InstanceType::M5adLarge\n    | InstanceType::M5adXlarge\n    | InstanceType::M5d12xlarge\n    | InstanceType::M5d16xlarge\n    | InstanceType::M5d24xlarge\n    | InstanceType::M5d2xlarge\n    | InstanceType::M5d4xlarge\n    | InstanceType::M5d8xlarge\n    | InstanceType::M5dLarge\n    | InstanceType::M5dMetal\n    | InstanceType::M5dXlarge\n    | InstanceType::M5dn12xlarge\n    | InstanceType::M5dn16xlarge\n    | InstanceType::M5dn24xlarge\n    | InstanceType::M5dn2xlarge\n    | InstanceType::M5dn4xlarge\n    | InstanceType::M5dn8xlarge\n    | InstanceType::M5dnLarge\n    | InstanceType::M5dnMetal\n    | InstanceType::M5dnXlarge\n    | InstanceType::M5n12xlarge\n    | InstanceType::M5n16xlarge\n    | InstanceType::M5n24xlarge\n    | InstanceType::M5n2xlarge\n    | InstanceType::M5n4xlarge\n    | InstanceType::M5n8xlarge\n    | InstanceType::M5nLarge\n    | InstanceType::M5nMetal\n    | InstanceType::M5nXlarge\n    | InstanceType::M5zn12xlarge\n    | InstanceType::M5zn2xlarge\n    | InstanceType::M5zn3xlarge\n    | InstanceType::M5zn6xlarge\n    | InstanceType::M5znLarge\n    | InstanceType::M5znMetal\n    | InstanceType::M5znXlarge\n    | InstanceType::M6a12xlarge\n    | InstanceType::M6a16xlarge\n    | InstanceType::M6a24xlarge\n    | InstanceType::M6a2xlarge\n    | InstanceType::M6a32xlarge\n    | InstanceType::M6a48xlarge\n    | InstanceType::M6a4xlarge\n    | InstanceType::M6a8xlarge\n    | InstanceType::M6aLarge\n    | InstanceType::M6aMetal\n    | InstanceType::M6aXlarge\n    | InstanceType::M6g12xlarge\n    | InstanceType::M6g16xlarge\n    | InstanceType::M6g2xlarge\n    | InstanceType::M6g4xlarge\n    | InstanceType::M6g8xlarge\n    | InstanceType::M6gLarge\n    | InstanceType::M6gMedium\n    | InstanceType::M6gMetal\n    | InstanceType::M6gXlarge\n    | InstanceType::M6gd12xlarge\n    | InstanceType::M6gd16xlarge\n    | InstanceType::M6gd2xlarge\n    | InstanceType::M6gd4xlarge\n    | InstanceType::M6gd8xlarge\n    | InstanceType::M6gdLarge\n    | InstanceType::M6gdMedium\n    | InstanceType::M6gdMetal\n    | InstanceType::M6gdXlarge\n    | InstanceType::M6i12xlarge\n    | InstanceType::M6i16xlarge\n    | InstanceType::M6i24xlarge\n    | InstanceType::M6i2xlarge\n    | InstanceType::M6i32xlarge\n    | InstanceType::M6i4xlarge\n    | InstanceType::M6i8xlarge\n    | InstanceType::M6iLarge\n    | InstanceType::M6iMetal\n    | InstanceType::M6iXlarge\n    | InstanceType::M6id12xlarge\n    | InstanceType::M6id16xlarge\n    | InstanceType::M6id24xlarge\n    | InstanceType::M6id2xlarge\n    | InstanceType::M6id32xlarge\n    | InstanceType::M6id4xlarge\n    | InstanceType::M6id8xlarge\n    | InstanceType::M6idLarge\n    | InstanceType::M6idMetal\n    | InstanceType::M6idXlarge\n    | InstanceType::M6idn12xlarge\n    | InstanceType::M6idn16xlarge\n    | InstanceType::M6idn24xlarge\n    | InstanceType::M6idn2xlarge\n    | InstanceType::M6idn32xlarge\n    | InstanceType::M6idn4xlarge\n    | InstanceType::M6idn8xlarge\n    | InstanceType::M6idnLarge\n    | InstanceType::M6idnMetal\n    | InstanceType::M6idnXlarge\n    | InstanceType::M6in12xlarge\n    | InstanceType::M6in16xlarge\n    | InstanceType::M6in24xlarge\n    | InstanceType::M6in2xlarge\n    | InstanceType::M6in32xlarge\n    | InstanceType::M6in4xlarge\n    | InstanceType::M6in8xlarge\n    | InstanceType::M6inLarge\n    | InstanceType::M6inMetal\n    | InstanceType::M6inXlarge\n    | InstanceType::M7a12xlarge\n    | InstanceType::M7a16xlarge\n    | InstanceType::M7a24xlarge\n    | InstanceType::M7a2xlarge\n    | InstanceType::M7a32xlarge\n    | InstanceType::M7a48xlarge\n    | InstanceType::M7a4xlarge\n    | InstanceType::M7a8xlarge\n    | InstanceType::M7aLarge\n    | InstanceType::M7aMedium\n    | InstanceType::M7aMetal48xl\n    | InstanceType::M7aXlarge\n    | InstanceType::M7g12xlarge\n    | InstanceType::M7g16xlarge\n    | InstanceType::M7g2xlarge\n    | InstanceType::M7g4xlarge\n    | InstanceType::M7g8xlarge\n    | InstanceType::M7gLarge\n    | InstanceType::M7gMedium\n    | InstanceType::M7gMetal\n    | InstanceType::M7gXlarge\n    | InstanceType::M7gd12xlarge\n    | InstanceType::M7gd16xlarge\n    | InstanceType::M7gd2xlarge\n    | InstanceType::M7gd4xlarge\n    | InstanceType::M7gd8xlarge\n    | InstanceType::M7gdLarge\n    | InstanceType::M7gdMedium\n    | InstanceType::M7gdXlarge\n    | InstanceType::M7iFlex2xlarge\n    | InstanceType::M7iFlex4xlarge\n    | InstanceType::M7iFlex8xlarge\n    | InstanceType::M7iFlexLarge\n    | InstanceType::M7iFlexXlarge\n    | InstanceType::M7i12xlarge\n    | InstanceType::M7i16xlarge\n    | InstanceType::M7i24xlarge\n    | InstanceType::M7i2xlarge\n    | InstanceType::M7i48xlarge\n    | InstanceType::M7i4xlarge\n    | InstanceType::M7i8xlarge\n    | InstanceType::M7iLarge\n    | InstanceType::M7iMetal24xl\n    | InstanceType::M7iMetal48xl\n    | InstanceType::M7iXlarge\n    | InstanceType::Mac1Metal\n    | InstanceType::Mac2M2Metal\n    | InstanceType::Mac2M2proMetal\n    | InstanceType::Mac2Metal\n    | InstanceType::P216xlarge\n    | InstanceType::P28xlarge\n    | InstanceType::P2Xlarge\n    | InstanceType::P316xlarge\n    | InstanceType::P32xlarge\n    | InstanceType::P38xlarge\n    | InstanceType::P3dn24xlarge\n    | InstanceType::P4d24xlarge\n    | InstanceType::P4de24xlarge\n    | InstanceType::P548xlarge\n    | InstanceType::R32xlarge\n    | InstanceType::R34xlarge\n    | InstanceType::R38xlarge\n    | InstanceType::R3Large\n    | InstanceType::R3Xlarge\n    | InstanceType::R416xlarge\n    | InstanceType::R42xlarge\n    | InstanceType::R44xlarge\n    | InstanceType::R48xlarge\n    | InstanceType::R4Large\n    | InstanceType::R4Xlarge\n    | InstanceType::R512xlarge\n    | InstanceType::R516xlarge\n    | InstanceType::R524xlarge\n    | InstanceType::R52xlarge\n    | InstanceType::R54xlarge\n    | InstanceType::R58xlarge\n    | InstanceType::R5Large\n    | InstanceType::R5Metal\n    | InstanceType::R5Xlarge\n    | InstanceType::R5a12xlarge\n    | InstanceType::R5a16xlarge\n    | InstanceType::R5a24xlarge\n    | InstanceType::R5a2xlarge\n    | InstanceType::R5a4xlarge\n    | InstanceType::R5a8xlarge\n    | InstanceType::R5aLarge\n    | InstanceType::R5aXlarge\n    | InstanceType::R5ad12xlarge\n    | InstanceType::R5ad16xlarge\n    | InstanceType::R5ad24xlarge\n    | InstanceType::R5ad2xlarge\n    | InstanceType::R5ad4xlarge\n    | InstanceType::R5ad8xlarge\n    | InstanceType::R5adLarge\n    | InstanceType::R5adXlarge\n    | InstanceType::R5b12xlarge\n    | InstanceType::R5b16xlarge\n    | InstanceType::R5b24xlarge\n    | InstanceType::R5b2xlarge\n    | InstanceType::R5b4xlarge\n    | InstanceType::R5b8xlarge\n    | InstanceType::R5bLarge\n    | InstanceType::R5bMetal\n    | InstanceType::R5bXlarge\n    | InstanceType::R5d12xlarge\n    | InstanceType::R5d16xlarge\n    | InstanceType::R5d24xlarge\n    | InstanceType::R5d2xlarge\n    | InstanceType::R5d4xlarge\n    | InstanceType::R5d8xlarge\n    | InstanceType::R5dLarge\n    | InstanceType::R5dMetal\n    | InstanceType::R5dXlarge\n    | InstanceType::R5dn12xlarge\n    | InstanceType::R5dn16xlarge\n    | InstanceType::R5dn24xlarge\n    | InstanceType::R5dn2xlarge\n    | InstanceType::R5dn4xlarge\n    | InstanceType::R5dn8xlarge\n    | InstanceType::R5dnLarge\n    | InstanceType::R5dnMetal\n    | InstanceType::R5dnXlarge\n    | InstanceType::R5n12xlarge\n    | InstanceType::R5n16xlarge\n    | InstanceType::R5n24xlarge\n    | InstanceType::R5n2xlarge\n    | InstanceType::R5n4xlarge\n    | InstanceType::R5n8xlarge\n    | InstanceType::R5nLarge\n    | InstanceType::R5nMetal\n    | InstanceType::R5nXlarge\n    | InstanceType::R6a12xlarge\n    | InstanceType::R6a16xlarge\n    | InstanceType::R6a24xlarge\n    | InstanceType::R6a2xlarge\n    | InstanceType::R6a32xlarge\n    | InstanceType::R6a48xlarge\n    | InstanceType::R6a4xlarge\n    | InstanceType::R6a8xlarge\n    | InstanceType::R6aLarge\n    | InstanceType::R6aMetal\n    | InstanceType::R6aXlarge\n    | InstanceType::R6g12xlarge\n    | InstanceType::R6g16xlarge\n    | InstanceType::R6g2xlarge\n    | InstanceType::R6g4xlarge\n    | InstanceType::R6g8xlarge\n    | InstanceType::R6gLarge\n    | InstanceType::R6gMedium\n    | InstanceType::R6gMetal\n    | InstanceType::R6gXlarge\n    | InstanceType::R6gd12xlarge\n    | InstanceType::R6gd16xlarge\n    | InstanceType::R6gd2xlarge\n    | InstanceType::R6gd4xlarge\n    | InstanceType::R6gd8xlarge\n    | InstanceType::R6gdLarge\n    | InstanceType::R6gdMedium\n    | InstanceType::R6gdMetal\n    | InstanceType::R6gdXlarge\n    | InstanceType::R6i12xlarge\n    | InstanceType::R6i16xlarge\n    | InstanceType::R6i24xlarge\n    | InstanceType::R6i2xlarge\n    | InstanceType::R6i32xlarge\n    | InstanceType::R6i4xlarge\n    | InstanceType::R6i8xlarge\n    | InstanceType::R6iLarge\n    | InstanceType::R6iMetal\n    | InstanceType::R6iXlarge\n    | InstanceType::R6id12xlarge\n    | InstanceType::R6id16xlarge\n    | InstanceType::R6id24xlarge\n    | InstanceType::R6id2xlarge\n    | InstanceType::R6id32xlarge\n    | InstanceType::R6id4xlarge\n    | InstanceType::R6id8xlarge\n    | InstanceType::R6idLarge\n    | InstanceType::R6idMetal\n    | InstanceType::R6idXlarge\n    | InstanceType::R6idn12xlarge\n    | InstanceType::R6idn16xlarge\n    | InstanceType::R6idn24xlarge\n    | InstanceType::R6idn2xlarge\n    | InstanceType::R6idn32xlarge\n    | InstanceType::R6idn4xlarge\n    | InstanceType::R6idn8xlarge\n    | InstanceType::R6idnLarge\n    | InstanceType::R6idnMetal\n    | InstanceType::R6idnXlarge\n    | InstanceType::R6in12xlarge\n    | InstanceType::R6in16xlarge\n    | InstanceType::R6in24xlarge\n    | InstanceType::R6in2xlarge\n    | InstanceType::R6in32xlarge\n    | InstanceType::R6in4xlarge\n    | InstanceType::R6in8xlarge\n    | InstanceType::R6inLarge\n    | InstanceType::R6inMetal\n    | InstanceType::R6inXlarge\n    | InstanceType::R7a12xlarge\n    | InstanceType::R7a16xlarge\n    | InstanceType::R7a24xlarge\n    | InstanceType::R7a2xlarge\n    | InstanceType::R7a32xlarge\n    | InstanceType::R7a48xlarge\n    | InstanceType::R7a4xlarge\n    | InstanceType::R7a8xlarge\n    | InstanceType::R7aLarge\n    | InstanceType::R7aMedium\n    | InstanceType::R7aMetal48xl\n    | InstanceType::R7aXlarge\n    | InstanceType::R7g12xlarge\n    | InstanceType::R7g16xlarge\n    | InstanceType::R7g2xlarge\n    | InstanceType::R7g4xlarge\n    | InstanceType::R7g8xlarge\n    | InstanceType::R7gLarge\n    | InstanceType::R7gMedium\n    | InstanceType::R7gMetal\n    | InstanceType::R7gXlarge\n    | InstanceType::R7gd12xlarge\n    | InstanceType::R7gd16xlarge\n    | InstanceType::R7gd2xlarge\n    | InstanceType::R7gd4xlarge\n    | InstanceType::R7gd8xlarge\n    | InstanceType::R7gdLarge\n    | InstanceType::R7gdMedium\n    | InstanceType::R7gdXlarge\n    | InstanceType::R7i12xlarge\n    | InstanceType::R7i16xlarge\n    | InstanceType::R7i24xlarge\n    | InstanceType::R7i2xlarge\n    | InstanceType::R7i48xlarge\n    | InstanceType::R7i4xlarge\n    | InstanceType::R7i8xlarge\n    | InstanceType::R7iLarge\n    | InstanceType::R7iMetal24xl\n    | InstanceType::R7iMetal48xl\n    | InstanceType::R7iXlarge\n    | InstanceType::R7iz12xlarge\n    | InstanceType::R7iz16xlarge\n    | InstanceType::R7iz2xlarge\n    | InstanceType::R7iz32xlarge\n    | InstanceType::R7iz4xlarge\n    | InstanceType::R7iz8xlarge\n    | InstanceType::R7izLarge\n    | InstanceType::R7izXlarge\n    | InstanceType::T1Micro\n    | InstanceType::T22xlarge\n    | InstanceType::T2Large\n    | InstanceType::T2Medium\n    | InstanceType::T2Micro\n    | InstanceType::T2Nano\n    | InstanceType::T2Small\n    | InstanceType::T2Xlarge\n    | InstanceType::T32xlarge\n    | InstanceType::T3Large\n    | InstanceType::T3Medium\n    | InstanceType::T3Micro\n    | InstanceType::T3Nano\n    | InstanceType::T3Small\n    | InstanceType::T3Xlarge\n    | InstanceType::T3a2xlarge\n    | InstanceType::T3aLarge\n    | InstanceType::T3aMedium\n    | InstanceType::T3aMicro\n    | InstanceType::T3aNano\n    | InstanceType::T3aSmall\n    | InstanceType::T3aXlarge\n    | InstanceType::T4g2xlarge\n    | InstanceType::T4gLarge\n    | InstanceType::T4gMedium\n    | InstanceType::T4gMicro\n    | InstanceType::T4gNano\n    | InstanceType::T4gSmall\n    | InstanceType::T4gXlarge\n    | InstanceType::Trn12xlarge\n    | InstanceType::Trn132xlarge\n    | InstanceType::Trn1n32xlarge\n    | InstanceType::U12tb1112xlarge\n    | InstanceType::U12tb1Metal\n    | InstanceType::U18tb1112xlarge\n    | InstanceType::U18tb1Metal\n    | InstanceType::U24tb1112xlarge\n    | InstanceType::U24tb1Metal\n    | InstanceType::U3tb156xlarge\n    | InstanceType::U6tb1112xlarge\n    | InstanceType::U6tb156xlarge\n    | InstanceType::U6tb1Metal\n    | InstanceType::U9tb1112xlarge\n    | InstanceType::U9tb1Metal\n    | InstanceType::Vt124xlarge\n    | InstanceType::Vt13xlarge\n    | InstanceType::Vt16xlarge\n    | InstanceType::X116xlarge\n    | InstanceType::X132xlarge\n    | InstanceType::X1e16xlarge\n    | InstanceType::X1e2xlarge\n    | InstanceType::X1e32xlarge\n    | InstanceType::X1e4xlarge\n    | InstanceType::X1e8xlarge\n    | InstanceType::X1eXlarge\n    | InstanceType::X2gd12xlarge\n    | InstanceType::X2gd16xlarge\n    | InstanceType::X2gd2xlarge\n    | InstanceType::X2gd4xlarge\n    | InstanceType::X2gd8xlarge\n    | InstanceType::X2gdLarge\n    | InstanceType::X2gdMedium\n    | InstanceType::X2gdMetal\n    | InstanceType::X2gdXlarge\n    | InstanceType::X2idn16xlarge\n    | InstanceType::X2idn24xlarge\n    | InstanceType::X2idn32xlarge\n    | InstanceType::X2idnMetal\n    | InstanceType::X2iedn16xlarge\n    | InstanceType::X2iedn24xlarge\n    | InstanceType::X2iedn2xlarge\n    | InstanceType::X2iedn32xlarge\n    | InstanceType::X2iedn4xlarge\n    | InstanceType::X2iedn8xlarge\n    | InstanceType::X2iednMetal\n    | InstanceType::X2iednXlarge\n    | InstanceType::X2iezn12xlarge\n    | InstanceType::X2iezn2xlarge\n    | InstanceType::X2iezn4xlarge\n    | InstanceType::X2iezn6xlarge\n    | InstanceType::X2iezn8xlarge\n    | InstanceType::X2ieznMetal\n    | InstanceType::Z1d12xlarge\n    | InstanceType::Z1d2xlarge\n    | InstanceType::Z1d3xlarge\n    | InstanceType::Z1d6xlarge\n    | InstanceType::Z1dLarge\n    | InstanceType::Z1dMetal\n    | InstanceType::Z1dXlarge\n    | InstanceType::C7gdMetal\n    | InstanceType::C7gnMetal\n    | InstanceType::C7iFlex2xlarge\n    | InstanceType::C7iFlex4xlarge\n    | InstanceType::C7iFlex8xlarge\n    | InstanceType::C7iFlexLarge\n    | InstanceType::C7iFlexXlarge\n    | InstanceType::C8g12xlarge\n    | InstanceType::C8g16xlarge\n    | InstanceType::C8g24xlarge\n    | InstanceType::C8g2xlarge\n    | InstanceType::C8g48xlarge\n    | InstanceType::C8g4xlarge\n    | InstanceType::C8g8xlarge\n    | InstanceType::C8gLarge\n    | InstanceType::C8gMedium\n    | InstanceType::C8gMetal24xl\n    | InstanceType::C8gMetal48xl\n    | InstanceType::C8gXlarge\n    | InstanceType::G612xlarge\n    | InstanceType::G616xlarge\n    | InstanceType::G624xlarge\n    | InstanceType::G62xlarge\n    | InstanceType::G648xlarge\n    | InstanceType::G64xlarge\n    | InstanceType::G68xlarge\n    | InstanceType::G6Xlarge\n    | InstanceType::G6e12xlarge\n    | InstanceType::G6e16xlarge\n    | InstanceType::G6e24xlarge\n    | InstanceType::G6e2xlarge\n    | InstanceType::G6e48xlarge\n    | InstanceType::G6e4xlarge\n    | InstanceType::G6e8xlarge\n    | InstanceType::G6eXlarge\n    | InstanceType::Gr64xlarge\n    | InstanceType::Gr68xlarge\n    | InstanceType::M7gdMetal\n    | InstanceType::M8g12xlarge\n    | InstanceType::M8g16xlarge\n    | InstanceType::M8g24xlarge\n    | InstanceType::M8g2xlarge\n    | InstanceType::M8g48xlarge\n    | InstanceType::M8g4xlarge\n    | InstanceType::M8g8xlarge\n    | InstanceType::M8gLarge\n    | InstanceType::M8gMedium\n    | InstanceType::M8gMetal24xl\n    | InstanceType::M8gMetal48xl\n    | InstanceType::M8gXlarge\n    | InstanceType::Mac2M1ultraMetal\n    | InstanceType::R7gdMetal\n    | InstanceType::R7izMetal16xl\n    | InstanceType::R7izMetal32xl\n    | InstanceType::R8g12xlarge\n    | InstanceType::R8g16xlarge\n    | InstanceType::R8g24xlarge\n    | InstanceType::R8g2xlarge\n    | InstanceType::R8g48xlarge\n    | InstanceType::R8g4xlarge\n    | InstanceType::R8g8xlarge\n    | InstanceType::R8gLarge\n    | InstanceType::R8gMedium\n    | InstanceType::R8gMetal24xl\n    | InstanceType::R8gMetal48xl\n    | InstanceType::R8gXlarge\n    | InstanceType::U7i12tb224xlarge\n    | InstanceType::U7ib12tb224xlarge\n    | InstanceType::U7in16tb224xlarge\n    | InstanceType::U7in24tb224xlarge\n    | InstanceType::U7in32tb224xlarge\n    | InstanceType::X8g12xlarge\n    | InstanceType::X8g16xlarge\n    | InstanceType::X8g24xlarge\n    | InstanceType::X8g2xlarge\n    | InstanceType::X8g48xlarge\n    | InstanceType::X8g4xlarge\n    | InstanceType::X8g8xlarge\n    | InstanceType::X8gLarge\n    | InstanceType::X8gMedium\n    | InstanceType::X8gMetal24xl\n    | InstanceType::X8gMetal48xl\n    | InstanceType::X8gXlarge => Ok(instance_type),\n    other => Err(anyhow!(\"unknown InstanceType: {other:?}\")),\n  }\n}\n"
  },
  {
    "path": "bin/core/src/cloud/aws/mod.rs",
    "content": "pub mod ec2;\n"
  },
  {
    "path": "bin/core/src/cloud/mod.rs",
    "content": "pub mod aws;\n\n#[derive(Debug)]\npub enum BuildCleanupData {\n  /// Nothing to clean up\n  Server,\n  /// Clean up AWS instance\n  Aws { instance_id: String, region: String },\n}\n"
  },
  {
    "path": "bin/core/src/config.rs",
    "content": "use std::{path::PathBuf, sync::OnceLock};\n\nuse anyhow::Context;\nuse colored::Colorize;\nuse config::ConfigLoader;\nuse environment_file::{\n  maybe_read_item_from_file, maybe_read_list_from_file,\n};\nuse komodo_client::entities::{\n  config::{\n    DatabaseConfig,\n    core::{\n      AwsCredentials, CoreConfig, Env, GithubWebhookAppConfig,\n      GithubWebhookAppInstallationConfig, OauthCredentials,\n    },\n  },\n  logger::LogConfig,\n};\n\npub fn core_config() -> &'static CoreConfig {\n  static CORE_CONFIG: OnceLock<CoreConfig> = OnceLock::new();\n  CORE_CONFIG.get_or_init(|| {\n    let env: Env = match envy::from_env()\n      .context(\"Failed to parse Komodo Core environment\") {\n        Ok(env) => env,\n        Err(e) => {\n          panic!(\"{e:?}\");\n        }\n      };\n    let config = if env.komodo_config_paths.is_empty() {\n      println!(\n        \"{}: No config paths found, using default config\",\n        \"INFO\".green(),\n      );\n      CoreConfig::default()\n    } else {\n      let config_keywords = env.komodo_config_keywords\n        .iter()\n        .map(String::as_str)\n        .collect::<Vec<_>>();\n      println!(\n        \"{}: {}: {config_keywords:?}\",\n        \"INFO\".green(),\n        \"Config File Keywords\".dimmed(),\n      );\n      (ConfigLoader {\n        paths: &env.komodo_config_paths\n          .iter()\n          .map(PathBuf::as_path)\n          .collect::<Vec<_>>(),\n        match_wildcards: &config_keywords,\n        include_file_name: \".kcoreinclude\",\n        merge_nested: env.komodo_merge_nested_config,\n        extend_array: env.komodo_extend_config_arrays,\n        debug_print: env.komodo_config_debug,\n      }).load::<CoreConfig>()\n      .expect(\"Failed at parsing config from paths\")\n    };\n\n    let installations = match (\n      maybe_read_list_from_file(\n        env.komodo_github_webhook_app_installations_ids_file,\n        env.komodo_github_webhook_app_installations_ids\n      ),\n      env.komodo_github_webhook_app_installations_namespaces\n    ) {\n      (Some(ids), Some(namespaces)) => {\n        if ids.len() != namespaces.len() {\n          panic!(\"KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_IDS length and KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_NAMESPACES length mismatch. Got {ids:?} and {namespaces:?}\")\n        }\n        ids\n          .into_iter()\n          .zip(namespaces)\n          .map(|(id, namespace)| GithubWebhookAppInstallationConfig {\n            id,\n            namespace\n          })\n          .collect()\n      },\n      (Some(_), None) | (None, Some(_)) => {\n        panic!(\"Got only one of KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_IDS or KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_NAMESPACES, both MUST be provided\");\n      }\n      (None, None) => {\n        config.github_webhook_app.installations\n      }\n    };\n\n    // recreating CoreConfig here makes sure apply all env overrides applied.\n    CoreConfig {\n      // Secret things overridden with file\n      jwt_secret: maybe_read_item_from_file(env.komodo_jwt_secret_file, env.komodo_jwt_secret).unwrap_or(config.jwt_secret),\n      passkey: maybe_read_item_from_file(env.komodo_passkey_file, env.komodo_passkey)\n        .unwrap_or(config.passkey),\n      webhook_secret: maybe_read_item_from_file(env.komodo_webhook_secret_file, env.komodo_webhook_secret)\n        .unwrap_or(config.webhook_secret),\n      database: DatabaseConfig {\n        uri: maybe_read_item_from_file(env.komodo_database_uri_file,env.komodo_database_uri).unwrap_or(config.database.uri),\n        address: env.komodo_database_address.unwrap_or(config.database.address),\n        username: maybe_read_item_from_file(env.komodo_database_username_file,env\n          .komodo_database_username)\n          .unwrap_or(config.database.username),\n        password: maybe_read_item_from_file(env.komodo_database_password_file,env\n          .komodo_database_password)\n          .unwrap_or(config.database.password),\n        app_name: env\n          .komodo_database_app_name\n          .unwrap_or(config.database.app_name),\n        db_name: env\n          .komodo_database_db_name\n          .unwrap_or(config.database.db_name),\n      },\n      init_admin_username: maybe_read_item_from_file(\n        env.komodo_init_admin_username_file,\n        env.komodo_init_admin_username\n      ).or(config.init_admin_username),\n      init_admin_password: maybe_read_item_from_file(\n        env.komodo_init_admin_password_file,\n        env.komodo_init_admin_password\n      ).unwrap_or(config.init_admin_password),\n      oidc_enabled: env.komodo_oidc_enabled.unwrap_or(config.oidc_enabled),\n      oidc_provider: env.komodo_oidc_provider.unwrap_or(config.oidc_provider),\n      oidc_redirect_host: env.komodo_oidc_redirect_host.unwrap_or(config.oidc_redirect_host),\n      oidc_client_id: maybe_read_item_from_file(env.komodo_oidc_client_id_file,env\n        .komodo_oidc_client_id)\n        .unwrap_or(config.oidc_client_id),\n      oidc_client_secret: maybe_read_item_from_file(env.komodo_oidc_client_secret_file,env\n        .komodo_oidc_client_secret)\n        .unwrap_or(config.oidc_client_secret),\n      oidc_use_full_email: env.komodo_oidc_use_full_email\n        .unwrap_or(config.oidc_use_full_email),\n      oidc_additional_audiences: maybe_read_list_from_file(env.komodo_oidc_additional_audiences_file,env\n        .komodo_oidc_additional_audiences)\n        .unwrap_or(config.oidc_additional_audiences),\n      google_oauth: OauthCredentials {\n        enabled: env\n          .komodo_google_oauth_enabled\n          .unwrap_or(config.google_oauth.enabled),\n        id: maybe_read_item_from_file(env.komodo_google_oauth_id_file,env\n          .komodo_google_oauth_id)\n          .unwrap_or(config.google_oauth.id),\n        secret: maybe_read_item_from_file(env.komodo_google_oauth_secret_file,env\n          .komodo_google_oauth_secret)\n          .unwrap_or(config.google_oauth.secret),\n      },\n      github_oauth: OauthCredentials {\n        enabled: env\n          .komodo_github_oauth_enabled\n          .unwrap_or(config.github_oauth.enabled),\n        id: maybe_read_item_from_file(env.komodo_github_oauth_id_file,env\n          .komodo_github_oauth_id)\n          .unwrap_or(config.github_oauth.id),\n        secret: maybe_read_item_from_file(env.komodo_github_oauth_secret_file,env\n          .komodo_github_oauth_secret)\n          .unwrap_or(config.github_oauth.secret),\n      },\n      aws: AwsCredentials {\n        access_key_id: maybe_read_item_from_file(env.komodo_aws_access_key_id_file, env\n          .komodo_aws_access_key_id)\n          .unwrap_or(config.aws.access_key_id),\n        secret_access_key: maybe_read_item_from_file(env.komodo_aws_secret_access_key_file, env\n          .komodo_aws_secret_access_key)\n          .unwrap_or(config.aws.secret_access_key),\n      },\n      github_webhook_app: GithubWebhookAppConfig {\n        app_id: maybe_read_item_from_file(env.komodo_github_webhook_app_app_id_file, env\n          .komodo_github_webhook_app_app_id)\n          .unwrap_or(config.github_webhook_app.app_id),\n        pk_path: env\n          .komodo_github_webhook_app_pk_path\n          .unwrap_or(config.github_webhook_app.pk_path),\n        installations,\n      },\n\n      // Non secrets\n      title: env.komodo_title.unwrap_or(config.title),\n      host: env.komodo_host.unwrap_or(config.host),\n      port: env.komodo_port.unwrap_or(config.port),\n      bind_ip: env.komodo_bind_ip.unwrap_or(config.bind_ip),\n      timezone: env.komodo_timezone.unwrap_or(config.timezone),\n      first_server: env.komodo_first_server.or(config.first_server),\n      first_server_name: env.komodo_first_server_name.unwrap_or(config.first_server_name),\n      frontend_path: env.komodo_frontend_path.unwrap_or(config.frontend_path),\n      jwt_ttl: env\n        .komodo_jwt_ttl\n        .unwrap_or(config.jwt_ttl),\n      sync_directory: env\n        .komodo_sync_directory\n        .unwrap_or(config.sync_directory),\n      repo_directory: env\n        .komodo_repo_directory\n        .unwrap_or(config.repo_directory),\n      action_directory: env\n        .komodo_action_directory\n        .unwrap_or(config.action_directory),\n      resource_poll_interval: env\n        .komodo_resource_poll_interval\n        .unwrap_or(config.resource_poll_interval),\n      monitoring_interval: env\n        .komodo_monitoring_interval\n        .unwrap_or(config.monitoring_interval),\n      keep_stats_for_days: env\n        .komodo_keep_stats_for_days\n        .unwrap_or(config.keep_stats_for_days),\n      keep_alerts_for_days: env\n        .komodo_keep_alerts_for_days\n        .unwrap_or(config.keep_alerts_for_days),\n      webhook_base_url: env\n        .komodo_webhook_base_url\n        .unwrap_or(config.webhook_base_url),\n      transparent_mode: env\n        .komodo_transparent_mode\n        .unwrap_or(config.transparent_mode),\n      ui_write_disabled: env\n        .komodo_ui_write_disabled\n        .unwrap_or(config.ui_write_disabled),\n      disable_confirm_dialog: env.komodo_disable_confirm_dialog\n        .unwrap_or(config.disable_confirm_dialog),\n      disable_websocket_reconnect: env.komodo_disable_websocket_reconnect\n        .unwrap_or(config.disable_websocket_reconnect),\n      enable_new_users: env.komodo_enable_new_users\n        .unwrap_or(config.enable_new_users),\n      disable_user_registration: env.komodo_disable_user_registration\n        .unwrap_or(config.disable_user_registration),\n      disable_non_admin_create: env.komodo_disable_non_admin_create\n        .unwrap_or(config.disable_non_admin_create),\n      disable_init_resources: env.komodo_disable_init_resources\n        .unwrap_or(config.disable_init_resources),\n      enable_fancy_toml: env.komodo_enable_fancy_toml\n        .unwrap_or(config.enable_fancy_toml),\n      lock_login_credentials_for: env.komodo_lock_login_credentials_for\n        .unwrap_or(config.lock_login_credentials_for),\n      local_auth: env.komodo_local_auth\n        .unwrap_or(config.local_auth),\n      logging: LogConfig {\n        level: env\n          .komodo_logging_level\n          .unwrap_or(config.logging.level),\n        stdio: env\n          .komodo_logging_stdio\n          .unwrap_or(config.logging.stdio),\n        pretty: env.komodo_logging_pretty\n          .unwrap_or(config.logging.pretty),\n        location: env.komodo_logging_location\n          .unwrap_or(config.logging.location),\n        otlp_endpoint: env\n          .komodo_logging_otlp_endpoint\n          .unwrap_or(config.logging.otlp_endpoint),\n        opentelemetry_service_name: env\n          .komodo_logging_opentelemetry_service_name\n          .unwrap_or(config.logging.opentelemetry_service_name),\n      },\n      pretty_startup_config: env.komodo_pretty_startup_config.unwrap_or(config.pretty_startup_config),\n      unsafe_unsanitized_startup_config: env.komodo_unsafe_unsanitized_startup_config.unwrap_or(config.unsafe_unsanitized_startup_config),\n      internet_interface: env.komodo_internet_interface.unwrap_or(config.internet_interface),\n      ssl_enabled: env.komodo_ssl_enabled.unwrap_or(config.ssl_enabled),\n      ssl_key_file: env.komodo_ssl_key_file.unwrap_or(config.ssl_key_file),\n      ssl_cert_file: env.komodo_ssl_cert_file.unwrap_or(config.ssl_cert_file),\n\n      // These can't be overridden on env\n      secrets: config.secrets,\n      git_providers: config.git_providers,\n      docker_registries: config.docker_registries,\n    }\n  })\n}\n"
  },
  {
    "path": "bin/core/src/helpers/action_state.rs",
    "content": "use std::sync::{Arc, Mutex};\n\nuse anyhow::anyhow;\nuse komodo_client::{\n  busy::Busy,\n  entities::{\n    action::ActionActionState, build::BuildActionState,\n    deployment::DeploymentActionState,\n    procedure::ProcedureActionState, repo::RepoActionState,\n    server::ServerActionState, stack::StackActionState,\n    sync::ResourceSyncActionState,\n  },\n};\n\nuse super::cache::Cache;\n\n#[derive(Default)]\npub struct ActionStates {\n  pub server: Cache<String, Arc<ActionState<ServerActionState>>>,\n  pub stack: Cache<String, Arc<ActionState<StackActionState>>>,\n  pub deployment:\n    Cache<String, Arc<ActionState<DeploymentActionState>>>,\n  pub build: Cache<String, Arc<ActionState<BuildActionState>>>,\n  pub repo: Cache<String, Arc<ActionState<RepoActionState>>>,\n  pub procedure:\n    Cache<String, Arc<ActionState<ProcedureActionState>>>,\n  pub action: Cache<String, Arc<ActionState<ActionActionState>>>,\n  pub sync: Cache<String, Arc<ActionState<ResourceSyncActionState>>>,\n}\n\n/// Need to be able to check \"busy\" with write lock acquired.\n#[derive(Default)]\npub struct ActionState<States: Default + Send + 'static>(\n  Mutex<States>,\n);\n\nimpl<States: Default + Busy + Copy + Send + 'static>\n  ActionState<States>\n{\n  pub fn get(&self) -> anyhow::Result<States> {\n    Ok(\n      *self\n        .0\n        .lock()\n        .map_err(|e| anyhow!(\"action state lock poisoned | {e:?}\"))?,\n    )\n  }\n\n  pub fn busy(&self) -> anyhow::Result<bool> {\n    Ok(\n      self\n        .0\n        .lock()\n        .map_err(|e| anyhow!(\"action state lock poisoned | {e:?}\"))?\n        .busy(),\n    )\n  }\n\n  /// Will acquire lock, check busy, and if not will\n  /// run the provided update function on the states.\n  /// Returns a guard that returns the states to default (not busy) when dropped.\n  pub fn update(\n    &self,\n    update_fn: impl Fn(&mut States),\n  ) -> anyhow::Result<UpdateGuard<'_, States>> {\n    self.update_custom(\n      update_fn,\n      |states| *states = Default::default(),\n      true,\n    )\n  }\n\n  /// Will acquire lock, optionally check busy, and if not will\n  /// run the provided update function on the states.\n  /// Returns a guard that calls the provided return_fn when dropped.\n  pub fn update_custom(\n    &self,\n    update_fn: impl Fn(&mut States),\n    return_fn: impl Fn(&mut States) + Send + 'static,\n    busy_check: bool,\n  ) -> anyhow::Result<UpdateGuard<'_, States>> {\n    let mut lock = self\n      .0\n      .lock()\n      .map_err(|e| anyhow!(\"Action state lock poisoned | {e:?}\"))?;\n    if busy_check && lock.busy() {\n      return Err(anyhow!(\"Resource is busy\"));\n    }\n    update_fn(&mut *lock);\n    Ok(UpdateGuard(&self.0, Box::new(return_fn)))\n  }\n}\n\n/// When dropped will return the inner state to default.\n/// The inner mutex guard must already be dropped BEFORE this is dropped,\n/// which is guaranteed as the inner guard is dropped by all public methods before\n/// user could drop UpdateGuard.\npub struct UpdateGuard<'a, States: Default + Send + 'static>(\n  &'a Mutex<States>,\n  Box<dyn Fn(&mut States) + Send>,\n);\n\nimpl<States: Default + Send + 'static> Drop\n  for UpdateGuard<'_, States>\n{\n  fn drop(&mut self) {\n    let mut lock = match self.0.lock() {\n      Ok(lock) => lock,\n      Err(e) => {\n        error!(\"CRITICAL: an action state lock is poisoned | {e:?}\");\n        return;\n      }\n    };\n    self.1(&mut *lock);\n  }\n}\n"
  },
  {
    "path": "bin/core/src/helpers/all_resources.rs",
    "content": "use std::collections::HashMap;\n\nuse komodo_client::entities::{\n  action::Action, alerter::Alerter, build::Build, builder::Builder,\n  deployment::Deployment, procedure::Procedure, repo::Repo,\n  server::Server, stack::Stack, sync::ResourceSync,\n};\n\n#[derive(Debug, Default)]\npub struct AllResourcesById {\n  pub servers: HashMap<String, Server>,\n  pub deployments: HashMap<String, Deployment>,\n  pub stacks: HashMap<String, Stack>,\n  pub builds: HashMap<String, Build>,\n  pub repos: HashMap<String, Repo>,\n  pub procedures: HashMap<String, Procedure>,\n  pub actions: HashMap<String, Action>,\n  pub builders: HashMap<String, Builder>,\n  pub alerters: HashMap<String, Alerter>,\n  pub syncs: HashMap<String, ResourceSync>,\n}\n\nimpl AllResourcesById {\n  /// Use `match_tags` to filter resources by tag.\n  pub async fn load() -> anyhow::Result<Self> {\n    let map = HashMap::new();\n    let id_to_tags = &map;\n    let match_tags = &[];\n    Ok(Self {\n      servers: crate::resource::get_id_to_resource_map::<Server>(\n        id_to_tags, match_tags,\n      )\n      .await?,\n      deployments: crate::resource::get_id_to_resource_map::<\n        Deployment,\n      >(id_to_tags, match_tags)\n      .await?,\n      builds: crate::resource::get_id_to_resource_map::<Build>(\n        id_to_tags, match_tags,\n      )\n      .await?,\n      repos: crate::resource::get_id_to_resource_map::<Repo>(\n        id_to_tags, match_tags,\n      )\n      .await?,\n      procedures:\n        crate::resource::get_id_to_resource_map::<Procedure>(\n          id_to_tags, match_tags,\n        )\n        .await?,\n      actions: crate::resource::get_id_to_resource_map::<Action>(\n        id_to_tags, match_tags,\n      )\n      .await?,\n      builders: crate::resource::get_id_to_resource_map::<Builder>(\n        id_to_tags, match_tags,\n      )\n      .await?,\n      alerters: crate::resource::get_id_to_resource_map::<Alerter>(\n        id_to_tags, match_tags,\n      )\n      .await?,\n      syncs: crate::resource::get_id_to_resource_map::<ResourceSync>(\n        id_to_tags, match_tags,\n      )\n      .await?,\n      stacks: crate::resource::get_id_to_resource_map::<Stack>(\n        id_to_tags, match_tags,\n      )\n      .await?,\n    })\n  }\n}\n"
  },
  {
    "path": "bin/core/src/helpers/builder.rs",
    "content": "use std::time::Duration;\n\nuse anyhow::{Context, anyhow};\nuse formatting::muted;\nuse komodo_client::entities::{\n  Version,\n  builder::{AwsBuilderConfig, Builder, BuilderConfig},\n  komodo_timestamp,\n  server::Server,\n  update::{Log, Update},\n};\nuse periphery_client::{\n  PeripheryClient,\n  api::{self, GetVersionResponse},\n};\n\nuse crate::{\n  cloud::{\n    BuildCleanupData,\n    aws::ec2::{\n      Ec2Instance, launch_ec2_instance,\n      terminate_ec2_instance_with_retry,\n    },\n  },\n  config::core_config,\n  helpers::update::update_update,\n  resource,\n};\n\nuse super::periphery_client;\n\nconst BUILDER_POLL_RATE_SECS: u64 = 2;\nconst BUILDER_POLL_MAX_TRIES: usize = 60;\n\n#[instrument(skip_all, fields(builder_id = builder.id, update_id = update.id))]\npub async fn get_builder_periphery(\n  // build: &Build,\n  resource_name: String,\n  version: Option<Version>,\n  builder: Builder,\n  update: &mut Update,\n) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {\n  match builder.config {\n    BuilderConfig::Url(config) => {\n      if config.address.is_empty() {\n        return Err(anyhow!(\n          \"Builder has not yet configured an address\"\n        ));\n      }\n      let periphery = PeripheryClient::new(\n        config.address,\n        if config.passkey.is_empty() {\n          core_config().passkey.clone()\n        } else {\n          config.passkey\n        },\n        Duration::from_secs(3),\n      );\n      periphery\n        .health_check()\n        .await\n        .context(\"Url Builder failed health check\")?;\n      Ok((periphery, BuildCleanupData::Server))\n    }\n    BuilderConfig::Server(config) => {\n      if config.server_id.is_empty() {\n        return Err(anyhow!(\"Builder has not configured a server\"));\n      }\n      let server = resource::get::<Server>(&config.server_id).await?;\n      let periphery = periphery_client(&server)?;\n      Ok((periphery, BuildCleanupData::Server))\n    }\n    BuilderConfig::Aws(config) => {\n      get_aws_builder(&resource_name, version, config, update).await\n    }\n  }\n}\n\n#[instrument(skip_all, fields(resource_name, update_id = update.id))]\nasync fn get_aws_builder(\n  resource_name: &str,\n  version: Option<Version>,\n  config: AwsBuilderConfig,\n  update: &mut Update,\n) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {\n  let start_create_ts = komodo_timestamp();\n\n  let version = version.map(|v| format!(\"-v{v}\")).unwrap_or_default();\n  let instance_name = format!(\"BUILDER-{resource_name}{version}\");\n  let Ec2Instance { instance_id, ip } =\n    launch_ec2_instance(&instance_name, &config).await?;\n\n  info!(\"ec2 instance launched\");\n\n  let log = Log {\n    stage: \"start build instance\".to_string(),\n    success: true,\n    stdout: start_aws_builder_log(&instance_id, &ip, &config),\n    start_ts: start_create_ts,\n    end_ts: komodo_timestamp(),\n    ..Default::default()\n  };\n\n  update.logs.push(log);\n\n  update_update(update.clone()).await?;\n\n  let protocol = if config.use_https { \"https\" } else { \"http\" };\n  let periphery_address =\n    format!(\"{protocol}://{ip}:{}\", config.port);\n  let periphery = PeripheryClient::new(\n    &periphery_address,\n    &core_config().passkey,\n    Duration::from_secs(3),\n  );\n\n  let start_connect_ts = komodo_timestamp();\n  let mut res = Ok(GetVersionResponse {\n    version: String::new(),\n  });\n  for _ in 0..BUILDER_POLL_MAX_TRIES {\n    let version = periphery\n      .request(api::GetVersion {})\n      .await\n      .context(\"failed to reach periphery client on builder\");\n    if let Ok(GetVersionResponse { version }) = &version {\n      let connect_log = Log {\n        stage: \"build instance connected\".to_string(),\n        success: true,\n        stdout: format!(\n          \"established contact with periphery on builder\\nperiphery version: v{version}\"\n        ),\n        start_ts: start_connect_ts,\n        end_ts: komodo_timestamp(),\n        ..Default::default()\n      };\n      update.logs.push(connect_log);\n      update_update(update.clone()).await?;\n      return Ok((\n        periphery,\n        BuildCleanupData::Aws {\n          instance_id,\n          region: config.region,\n        },\n      ));\n    }\n    res = version;\n    tokio::time::sleep(Duration::from_secs(BUILDER_POLL_RATE_SECS))\n      .await;\n  }\n\n  // Spawn terminate task in failure case (if loop is passed without return)\n  tokio::spawn(async move {\n    let _ =\n      terminate_ec2_instance_with_retry(config.region, &instance_id)\n        .await;\n  });\n\n  // Unwrap is safe, only way to get here is after check Ok / early return, so it must be err\n  Err(\n    res.err().unwrap().context(\n      \"failed to start usable builder. terminating instance.\",\n    ),\n  )\n}\n\n#[instrument(skip(update))]\npub async fn cleanup_builder_instance(\n  cleanup_data: BuildCleanupData,\n  update: &mut Update,\n) {\n  match cleanup_data {\n    BuildCleanupData::Server => {\n      // Nothing to clean up\n    }\n    BuildCleanupData::Aws {\n      instance_id,\n      region,\n    } => {\n      let _instance_id = instance_id.clone();\n      tokio::spawn(async move {\n        let _ =\n          terminate_ec2_instance_with_retry(region, &_instance_id)\n            .await;\n      });\n      update.push_simple_log(\n        \"terminate instance\",\n        format!(\"termination queued for instance id {instance_id}\"),\n      );\n    }\n  }\n}\n\npub fn start_aws_builder_log(\n  instance_id: &str,\n  ip: &str,\n  config: &AwsBuilderConfig,\n) -> String {\n  let AwsBuilderConfig {\n    ami_id,\n    instance_type,\n    volume_gb,\n    subnet_id,\n    assign_public_ip,\n    security_group_ids,\n    use_public_ip,\n    use_https,\n    ..\n  } = config;\n\n  let readable_sec_group_ids = security_group_ids.join(\", \");\n\n  [\n    format!(\"{}: {instance_id}\", muted(\"instance id\")),\n    format!(\"{}: {ip}\", muted(\"ip\")),\n    format!(\"{}: {ami_id}\", muted(\"ami id\")),\n    format!(\"{}: {instance_type}\", muted(\"instance type\")),\n    format!(\"{}: {volume_gb} GB\", muted(\"volume size\")),\n    format!(\"{}: {subnet_id}\", muted(\"subnet id\")),\n    format!(\"{}: {readable_sec_group_ids}\", muted(\"security groups\")),\n    format!(\"{}: {assign_public_ip}\", muted(\"assign public ip\")),\n    format!(\"{}: {use_public_ip}\", muted(\"use public ip\")),\n    format!(\"{}: {use_https}\", muted(\"use https\")),\n  ]\n  .join(\"\\n\")\n}\n"
  },
  {
    "path": "bin/core/src/helpers/cache.rs",
    "content": "use std::{collections::HashMap, hash::Hash};\n\nuse tokio::sync::RwLock;\n\n#[derive(Default)]\npub struct Cache<K: PartialEq + Eq + Hash, T: Clone + Default> {\n  cache: RwLock<HashMap<K, T>>,\n}\n\nimpl<\n  K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,\n  T: Clone + Default,\n> Cache<K, T>\n{\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn get(&self, key: &K) -> Option<T> {\n    self.cache.read().await.get(key).cloned()\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn get_or_insert_default(&self, key: &K) -> T {\n    let mut lock = self.cache.write().await;\n    match lock.get(key).cloned() {\n      Some(item) => item,\n      None => {\n        let item: T = Default::default();\n        lock.insert(key.clone(), item.clone());\n        item\n      }\n    }\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn get_list(&self) -> Vec<T> {\n    let cache = self.cache.read().await;\n    cache.values().cloned().collect()\n  }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn insert<Key>(&self, key: Key, val: T)\n  where\n    T: std::fmt::Debug,\n    Key: Into<K> + std::fmt::Debug,\n  {\n    self.cache.write().await.insert(key.into(), val);\n  }\n\n  // #[instrument(level = \"debug\", skip(self, handler))]\n  // pub async fn update_entry<Key>(\n  //   &self,\n  //   key: Key,\n  //   handler: impl Fn(&mut T),\n  // ) where\n  //   Key: Into<K> + std::fmt::Debug,\n  // {\n  //   let mut cache = self.cache.write().await;\n  //   handler(cache.entry(key.into()).or_default());\n  // }\n\n  // #[instrument(level = \"debug\", skip(self))]\n  // pub async fn clear(&self) {\n  //   self.cache.write().await.clear();\n  // }\n\n  #[instrument(level = \"debug\", skip(self))]\n  pub async fn remove(&self, key: &K) {\n    self.cache.write().await.remove(key);\n  }\n}\n\n// impl<\n//   K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,\n//   T: Clone + Default + Busy,\n// > Cache<K, T>\n// {\n//   #[instrument(level = \"debug\", skip(self))]\n//   pub async fn busy(&self, id: &K) -> bool {\n//     match self.get(id).await {\n//       Some(state) => state.busy(),\n//       None => false,\n//     }\n//   }\n// }\n"
  },
  {
    "path": "bin/core/src/helpers/channel.rs",
    "content": "use std::sync::OnceLock;\n\nuse komodo_client::entities::update::{Update, UpdateListItem};\nuse tokio::sync::{Mutex, broadcast};\n\n/// A channel sending (build_id, update_id)\npub fn build_cancel_channel()\n-> &'static BroadcastChannel<(String, Update)> {\n  static BUILD_CANCEL_CHANNEL: OnceLock<\n    BroadcastChannel<(String, Update)>,\n  > = OnceLock::new();\n  BUILD_CANCEL_CHANNEL.get_or_init(|| BroadcastChannel::new(100))\n}\n\n/// A channel sending (repo_id, update_id)\npub fn repo_cancel_channel()\n-> &'static BroadcastChannel<(String, Update)> {\n  static REPO_CANCEL_CHANNEL: OnceLock<\n    BroadcastChannel<(String, Update)>,\n  > = OnceLock::new();\n  REPO_CANCEL_CHANNEL.get_or_init(|| BroadcastChannel::new(100))\n}\n\npub fn update_channel() -> &'static BroadcastChannel<UpdateListItem> {\n  static UPDATE_CHANNEL: OnceLock<BroadcastChannel<UpdateListItem>> =\n    OnceLock::new();\n  UPDATE_CHANNEL.get_or_init(|| BroadcastChannel::new(100))\n}\n\npub struct BroadcastChannel<T> {\n  pub sender: Mutex<broadcast::Sender<T>>,\n  pub receiver: broadcast::Receiver<T>,\n}\n\nimpl<T: Clone> BroadcastChannel<T> {\n  pub fn new(capacity: usize) -> BroadcastChannel<T> {\n    let (sender, receiver) = broadcast::channel(capacity);\n    BroadcastChannel {\n      sender: sender.into(),\n      receiver,\n    }\n  }\n}\n"
  },
  {
    "path": "bin/core/src/helpers/maintenance.rs",
    "content": "use std::str::FromStr;\n\nuse anyhow::Context;\nuse chrono::{Datelike, Local};\nuse komodo_client::entities::{\n  DayOfWeek, MaintenanceScheduleType, MaintenanceWindow,\n};\n\nuse crate::config::core_config;\n\n/// Check if a timestamp is currently in a maintenance window, given a list of windows.\npub fn is_in_maintenance(\n  windows: &[MaintenanceWindow],\n  timestamp: i64,\n) -> bool {\n  windows\n    .iter()\n    .any(|window| is_maintenance_window_active(window, timestamp))\n}\n\n/// Check if the current timestamp falls within this maintenance window\npub fn is_maintenance_window_active(\n  window: &MaintenanceWindow,\n  timestamp: i64,\n) -> bool {\n  if !window.enabled {\n    return false;\n  }\n\n  let dt = chrono::DateTime::from_timestamp(timestamp / 1000, 0)\n    .unwrap_or_else(chrono::Utc::now);\n\n  let (local_time, local_weekday, local_date) =\n    match (window.timezone.as_str(), core_config().timezone.as_str())\n    {\n      (\"\", \"\") => {\n        let local_dt = dt.with_timezone(&Local);\n        (local_dt.time(), local_dt.weekday(), local_dt.date_naive())\n      }\n      (\"\", timezone) | (timezone, _) => {\n        let tz: chrono_tz::Tz = match timezone\n          .parse()\n          .context(\"Failed to parse timezone\")\n        {\n          Ok(tz) => tz,\n          Err(e) => {\n            warn!(\n              \"Failed to parse maintenance window timezone: {e:#}\"\n            );\n            return false;\n          }\n        };\n        let local_dt = dt.with_timezone(&tz);\n        (local_dt.time(), local_dt.weekday(), local_dt.date_naive())\n      }\n    };\n\n  match window.schedule_type {\n    MaintenanceScheduleType::Daily => {\n      is_time_in_window(window, local_time)\n    }\n    MaintenanceScheduleType::Weekly => {\n      let day_of_week =\n        DayOfWeek::from_str(&window.day_of_week).unwrap_or_default();\n      convert_day_of_week(local_weekday) == day_of_week\n        && is_time_in_window(window, local_time)\n    }\n    MaintenanceScheduleType::OneTime => {\n      // Parse the date string and check if it matches current date\n      if let Ok(maintenance_date) =\n        chrono::NaiveDate::parse_from_str(&window.date, \"%Y-%m-%d\")\n      {\n        local_date == maintenance_date\n          && is_time_in_window(window, local_time)\n      } else {\n        false\n      }\n    }\n  }\n}\n\nfn is_time_in_window(\n  window: &MaintenanceWindow,\n  current_time: chrono::NaiveTime,\n) -> bool {\n  let start_time = chrono::NaiveTime::from_hms_opt(\n    window.hour as u32,\n    window.minute as u32,\n    0,\n  )\n  .unwrap_or(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap());\n\n  let end_time = start_time\n    + chrono::Duration::minutes(window.duration_minutes as i64);\n\n  // Handle case where maintenance window crosses midnight\n  if end_time < start_time {\n    current_time >= start_time || current_time <= end_time\n  } else {\n    current_time >= start_time && current_time <= end_time\n  }\n}\n\nfn convert_day_of_week(value: chrono::Weekday) -> DayOfWeek {\n  match value {\n    chrono::Weekday::Mon => DayOfWeek::Monday,\n    chrono::Weekday::Tue => DayOfWeek::Tuesday,\n    chrono::Weekday::Wed => DayOfWeek::Wednesday,\n    chrono::Weekday::Thu => DayOfWeek::Thursday,\n    chrono::Weekday::Fri => DayOfWeek::Friday,\n    chrono::Weekday::Sat => DayOfWeek::Saturday,\n    chrono::Weekday::Sun => DayOfWeek::Sunday,\n  }\n}\n"
  },
  {
    "path": "bin/core/src/helpers/matcher.rs",
    "content": "use anyhow::Context;\n\npub enum Matcher<'a> {\n  Wildcard(wildcard::Wildcard<'a>),\n  Regex(regex::Regex),\n}\n\nimpl<'a> Matcher<'a> {\n  pub fn new(pattern: &'a str) -> anyhow::Result<Self> {\n    if pattern.starts_with('\\\\') && pattern.ends_with('\\\\') {\n      let inner = &pattern[1..(pattern.len() - 1)];\n      let regex = regex::Regex::new(inner)\n        .with_context(|| format!(\"invalid regex. got: {inner}\"))?;\n      Ok(Self::Regex(regex))\n    } else {\n      let wildcard = wildcard::Wildcard::new(pattern.as_bytes())\n        .with_context(|| {\n          format!(\"invalid wildcard. got: {pattern}\")\n        })?;\n      Ok(Self::Wildcard(wildcard))\n    }\n  }\n\n  pub fn is_match(&self, source: &str) -> bool {\n    match self {\n      Matcher::Wildcard(wildcard) => {\n        wildcard.is_match(source.as_bytes())\n      }\n      Matcher::Regex(regex) => regex.is_match(source),\n    }\n  }\n}\n"
  },
  {
    "path": "bin/core/src/helpers/mod.rs",
    "content": "use std::{fmt::Write, time::Duration};\n\nuse anyhow::{Context, anyhow};\nuse database::mongo_indexed::Document;\nuse database::mungos::mongodb::bson::{Bson, doc};\nuse indexmap::IndexSet;\nuse komodo_client::entities::{\n  ResourceTarget,\n  build::Build,\n  permission::{\n    Permission, PermissionLevel, SpecificPermission, UserTarget,\n  },\n  repo::Repo,\n  server::Server,\n  stack::Stack,\n  user::User,\n};\nuse periphery_client::PeripheryClient;\nuse rand::Rng;\n\nuse crate::{config::core_config, state::db_client};\n\npub mod action_state;\npub mod all_resources;\npub mod builder;\npub mod cache;\npub mod channel;\npub mod maintenance;\npub mod matcher;\npub mod procedure;\npub mod prune;\npub mod query;\npub mod update;\n\n// pub mod resource;\n\npub fn empty_or_only_spaces(word: &str) -> bool {\n  if word.is_empty() {\n    return true;\n  }\n  for char in word.chars() {\n    if char != ' ' {\n      return false;\n    }\n  }\n  true\n}\n\npub fn random_string(length: usize) -> String {\n  rand::rng()\n    .sample_iter(&rand::distr::Alphanumeric)\n    .take(length)\n    .map(char::from)\n    .collect()\n}\n\n/// First checks db for token, then checks core config.\n/// Only errors if db call errors.\n/// Returns (token, use_https)\npub async fn git_token(\n  provider_domain: &str,\n  account_username: &str,\n  mut on_https_found: impl FnMut(bool),\n) -> anyhow::Result<Option<String>> {\n  if provider_domain.is_empty() || account_username.is_empty() {\n    return Ok(None);\n  }\n  let db_provider = db_client()\n    .git_accounts\n    .find_one(doc! { \"domain\": provider_domain, \"username\": account_username })\n    .await\n    .context(\"failed to query db for git provider accounts\")?;\n  if let Some(provider) = db_provider {\n    on_https_found(provider.https);\n    return Ok(Some(provider.token));\n  }\n  Ok(\n    core_config()\n      .git_providers\n      .iter()\n      .find(|provider| provider.domain == provider_domain)\n      .and_then(|provider| {\n        on_https_found(provider.https);\n        provider\n          .accounts\n          .iter()\n          .find(|account| account.username == account_username)\n          .map(|account| account.token.clone())\n      }),\n  )\n}\n\npub async fn stack_git_token(\n  stack: &mut Stack,\n  repo: Option<&mut Repo>,\n) -> anyhow::Result<Option<String>> {\n  if let Some(repo) = repo {\n    return git_token(\n      &repo.config.git_provider,\n      &repo.config.git_account,\n      |https| repo.config.git_https = https,\n    )\n    .await\n    .with_context(|| {\n      format!(\n        \"Failed to get git token. Stopping run. | {} | {}\",\n        repo.config.git_provider, repo.config.git_account\n      )\n    });\n  }\n  git_token(\n    &stack.config.git_provider,\n    &stack.config.git_account,\n    |https| stack.config.git_https = https,\n  )\n  .await\n  .with_context(|| {\n    format!(\n      \"Failed to get git token. Stopping run. | {} | {}\",\n      stack.config.git_provider, stack.config.git_account\n    )\n  })\n}\n\npub async fn build_git_token(\n  build: &mut Build,\n  repo: Option<&mut Repo>,\n) -> anyhow::Result<Option<String>> {\n  if let Some(repo) = repo {\n    return git_token(\n      &repo.config.git_provider,\n      &repo.config.git_account,\n      |https| repo.config.git_https = https,\n    )\n    .await\n    .with_context(|| {\n      format!(\n        \"Failed to get git token. Stopping run. | {} | {}\",\n        repo.config.git_provider, repo.config.git_account\n      )\n    });\n  }\n  git_token(\n    &build.config.git_provider,\n    &build.config.git_account,\n    |https| build.config.git_https = https,\n  )\n  .await\n  .with_context(|| {\n    format!(\n      \"Failed to get git token. Stopping run. | {} | {}\",\n      build.config.git_provider, build.config.git_account\n    )\n  })\n}\n\n/// First checks db for token, then checks core config.\n/// Only errors if db call errors.\npub async fn registry_token(\n  provider_domain: &str,\n  account_username: &str,\n) -> anyhow::Result<Option<String>> {\n  let provider = db_client()\n    .registry_accounts\n    .find_one(doc! { \"domain\": provider_domain, \"username\": account_username })\n    .await\n    .context(\"failed to query db for docker registry accounts\")?;\n  if let Some(provider) = provider {\n    return Ok(Some(provider.token));\n  }\n  Ok(\n    core_config()\n      .docker_registries\n      .iter()\n      .find(|provider| provider.domain == provider_domain)\n      .and_then(|provider| {\n        provider\n          .accounts\n          .iter()\n          .find(|account| account.username == account_username)\n          .map(|account| account.token.clone())\n      }),\n  )\n}\n\n//\n\npub fn periphery_client(\n  server: &Server,\n) -> anyhow::Result<PeripheryClient> {\n  if !server.config.enabled {\n    return Err(anyhow!(\"server not enabled\"));\n  }\n\n  let client = PeripheryClient::new(\n    &server.config.address,\n    if server.config.passkey.is_empty() {\n      &core_config().passkey\n    } else {\n      &server.config.passkey\n    },\n    Duration::from_secs(server.config.timeout_seconds as u64),\n  );\n\n  Ok(client)\n}\n\n#[instrument]\npub async fn create_permission<T>(\n  user: &User,\n  target: T,\n  level: PermissionLevel,\n  specific: IndexSet<SpecificPermission>,\n) where\n  T: Into<ResourceTarget> + std::fmt::Debug,\n{\n  // No need to actually create permissions for admins\n  if user.admin {\n    return;\n  }\n  let target: ResourceTarget = target.into();\n  if let Err(e) = db_client()\n    .permissions\n    .insert_one(Permission {\n      id: Default::default(),\n      user_target: UserTarget::User(user.id.clone()),\n      resource_target: target.clone(),\n      level,\n      specific,\n    })\n    .await\n  {\n    error!(\"failed to create permission for {target:?} | {e:#}\");\n  };\n}\n\n/// Flattens a document only one level deep\n///\n/// eg `{ config: { label: \"yes\", thing: { field1: \"ok\", field2: \"ok\" } } }` ->\n/// `{ \"config.label\": \"yes\", \"config.thing\": { field1: \"ok\", field2: \"ok\" } }`\npub fn flatten_document(doc: Document) -> Document {\n  let mut target = Document::new();\n\n  for (outer_field, bson) in doc {\n    if let Bson::Document(doc) = bson {\n      for (inner_field, bson) in doc {\n        target.insert(format!(\"{outer_field}.{inner_field}\"), bson);\n      }\n    } else {\n      target.insert(outer_field, bson);\n    }\n  }\n\n  target\n}\n\npub fn repo_link(\n  provider: &str,\n  repo: &str,\n  branch: &str,\n  https: bool,\n) -> String {\n  let mut res = format!(\n    \"http{}://{provider}/{repo}\",\n    if https { \"s\" } else { \"\" }\n  );\n  // Each provider uses a different link format to get to branches.\n  // At least can support github for branch aware link.\n  if provider == \"github.com\" {\n    let _ = write!(&mut res, \"/tree/{branch}\");\n  }\n  res\n}\n"
  },
  {
    "path": "bin/core/src/helpers/procedure.rs",
    "content": "use std::time::{Duration, Instant};\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::by_id::find_one_by_id;\nuse formatting::{Color, bold, colored, format_serror, muted};\nuse futures::future::join_all;\nuse komodo_client::{\n  api::execute::*,\n  entities::{\n    action::Action,\n    build::Build,\n    deployment::Deployment,\n    permission::PermissionLevel,\n    procedure::Procedure,\n    repo::Repo,\n    stack::Stack,\n    update::{Log, Update},\n    user::procedure_user,\n  },\n};\nuse resolver_api::Resolve;\nuse tokio::sync::Mutex;\n\nuse crate::{\n  api::{\n    execute::{ExecuteArgs, ExecuteRequest},\n    write::WriteArgs,\n  },\n  resource::{KomodoResource, list_full_for_user_using_pattern},\n  state::db_client,\n};\n\nuse super::update::{init_execution_update, update_update};\n\n#[instrument(skip_all)]\npub async fn execute_procedure(\n  procedure: &Procedure,\n  update: &Mutex<Update>,\n) -> anyhow::Result<()> {\n  for stage in &procedure.config.stages {\n    if !stage.enabled {\n      continue;\n    }\n    add_line_to_update(\n      update,\n      &format!(\n        \"{}: Executing stage: '{}'\",\n        muted(\"INFO\"),\n        bold(&stage.name)\n      ),\n    )\n    .await;\n    let timer = Instant::now();\n    execute_stage(\n      stage\n        .executions\n        .iter()\n        .filter(|item| item.enabled)\n        .map(|item| item.execution.clone())\n        .collect(),\n      &procedure.id,\n      &procedure.name,\n      update,\n    )\n    .await\n    .with_context(|| {\n      format!(\n        \"Failed stage '{}' execution after {:?}\",\n        bold(&stage.name),\n        timer.elapsed(),\n      )\n    })?;\n    add_line_to_update(\n      update,\n      &format!(\n        \"{}: {} stage '{}' execution in {:?}\",\n        muted(\"INFO\"),\n        colored(\"Finished\", Color::Green),\n        bold(&stage.name),\n        timer.elapsed()\n      ),\n    )\n    .await;\n  }\n\n  Ok(())\n}\n\n#[allow(dependency_on_unit_never_type_fallback)]\n#[instrument(skip(update))]\nasync fn execute_stage(\n  _executions: Vec<Execution>,\n  parent_id: &str,\n  parent_name: &str,\n  update: &Mutex<Update>,\n) -> anyhow::Result<()> {\n  let mut executions = Vec::with_capacity(_executions.capacity());\n  for execution in _executions {\n    match execution {\n      Execution::BatchRunAction(exec) => {\n        extend_batch_exection::<BatchRunAction>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchRunProcedure(exec) => {\n        extend_batch_exection::<BatchRunProcedure>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchRunBuild(exec) => {\n        extend_batch_exection::<BatchRunBuild>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchCloneRepo(exec) => {\n        extend_batch_exection::<BatchCloneRepo>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchPullRepo(exec) => {\n        extend_batch_exection::<BatchPullRepo>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchBuildRepo(exec) => {\n        extend_batch_exection::<BatchBuildRepo>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchDeploy(exec) => {\n        extend_batch_exection::<BatchDeploy>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchDestroyDeployment(exec) => {\n        extend_batch_exection::<BatchDestroyDeployment>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchDeployStack(exec) => {\n        extend_batch_exection::<BatchDeployStack>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchDeployStackIfChanged(exec) => {\n        extend_batch_exection::<BatchDeployStackIfChanged>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchPullStack(exec) => {\n        extend_batch_exection::<BatchPullStack>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      Execution::BatchDestroyStack(exec) => {\n        extend_batch_exection::<BatchDestroyStack>(\n          &exec.pattern,\n          &mut executions,\n        )\n        .await?;\n      }\n      execution => executions.push(execution),\n    }\n  }\n  let futures = executions.into_iter().map(|execution| async move {\n    let now = Instant::now();\n    add_line_to_update(\n      update,\n      &format!(\"{}: Executing: {execution:?}\", muted(\"INFO\")),\n    )\n    .await;\n    let fail_log = format!(\n      \"{}: Failed on {execution:?}\",\n      colored(\"ERROR\", Color::Red)\n    );\n    let res =\n      execute_execution(execution.clone(), parent_id, parent_name)\n        .await\n        .context(fail_log);\n    add_line_to_update(\n      update,\n      &format!(\n        \"{}: {} execution in {:?}: {execution:?}\",\n        muted(\"INFO\"),\n        colored(\"Finished\", Color::Green),\n        now.elapsed()\n      ),\n    )\n    .await;\n    res\n  });\n  join_all(futures)\n    .await\n    .into_iter()\n    .collect::<anyhow::Result<Vec<_>>>()?;\n  Ok(())\n}\n\nasync fn execute_execution(\n  execution: Execution,\n  // used to prevent recursive procedure\n  parent_id: &str,\n  parent_name: &str,\n) -> anyhow::Result<()> {\n  let user = procedure_user().to_owned();\n  let update = match execution {\n    Execution::None(_) => return Ok(()),\n    Execution::RunProcedure(req) => {\n      if req.procedure == parent_id || req.procedure == parent_name {\n        return Err(anyhow!(\"Self referential procedure detected\"));\n      }\n      let req = ExecuteRequest::RunProcedure(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::RunProcedure(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RunProcedure\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchRunProcedure(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchRunProcedure not implemented correctly\"\n      ));\n    }\n    Execution::RunAction(req) => {\n      let req = ExecuteRequest::RunAction(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::RunAction(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RunAction\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchRunAction(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchRunAction not implemented correctly\"\n      ));\n    }\n    Execution::RunBuild(req) => {\n      let req = ExecuteRequest::RunBuild(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::RunBuild(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RunBuild\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchRunBuild(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchRunBuild not implemented correctly\"\n      ));\n    }\n    Execution::CancelBuild(req) => {\n      let req = ExecuteRequest::CancelBuild(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::CancelBuild(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at CancelBuild\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::Deploy(req) => {\n      let req = ExecuteRequest::Deploy(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::Deploy(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at Deploy\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchDeploy(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchDeploy not implemented correctly\"\n      ));\n    }\n    Execution::PullDeployment(req) => {\n      let req = ExecuteRequest::PullDeployment(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PullDeployment(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PullDeployment\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::StartDeployment(req) => {\n      let req = ExecuteRequest::StartDeployment(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::StartDeployment(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at StartDeployment\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::RestartDeployment(req) => {\n      let req = ExecuteRequest::RestartDeployment(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::RestartDeployment(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RestartDeployment\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PauseDeployment(req) => {\n      let req = ExecuteRequest::PauseDeployment(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PauseDeployment(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PauseDeployment\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::UnpauseDeployment(req) => {\n      let req = ExecuteRequest::UnpauseDeployment(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::UnpauseDeployment(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at UnpauseDeployment\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::StopDeployment(req) => {\n      let req = ExecuteRequest::StopDeployment(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::StopDeployment(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at StopDeployment\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::DestroyDeployment(req) => {\n      let req = ExecuteRequest::DestroyDeployment(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::DestroyDeployment(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RemoveDeployment\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchDestroyDeployment(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchDestroyDeployment not implemented correctly\"\n      ));\n    }\n    Execution::CloneRepo(req) => {\n      let req = ExecuteRequest::CloneRepo(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::CloneRepo(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at CloneRepo\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchCloneRepo(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchCloneRepo not implemented correctly\"\n      ));\n    }\n    Execution::PullRepo(req) => {\n      let req = ExecuteRequest::PullRepo(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PullRepo(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PullRepo\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchPullRepo(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchPullRepo not implemented correctly\"\n      ));\n    }\n    Execution::BuildRepo(req) => {\n      let req = ExecuteRequest::BuildRepo(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::BuildRepo(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at BuildRepo\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchBuildRepo(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchBuildRepo not implemented correctly\"\n      ));\n    }\n    Execution::CancelRepoBuild(req) => {\n      let req = ExecuteRequest::CancelRepoBuild(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::CancelRepoBuild(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at CancelRepoBuild\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::StartContainer(req) => {\n      let req = ExecuteRequest::StartContainer(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::StartContainer(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at StartContainer\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::RestartContainer(req) => {\n      let req = ExecuteRequest::RestartContainer(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::RestartContainer(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RestartContainer\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PauseContainer(req) => {\n      let req = ExecuteRequest::PauseContainer(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PauseContainer(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PauseContainer\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::UnpauseContainer(req) => {\n      let req = ExecuteRequest::UnpauseContainer(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::UnpauseContainer(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at UnpauseContainer\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::StopContainer(req) => {\n      let req = ExecuteRequest::StopContainer(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::StopContainer(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at StopContainer\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::DestroyContainer(req) => {\n      let req = ExecuteRequest::DestroyContainer(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::DestroyContainer(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RemoveContainer\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::StartAllContainers(req) => {\n      let req = ExecuteRequest::StartAllContainers(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::StartAllContainers(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at StartAllContainers\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::RestartAllContainers(req) => {\n      let req = ExecuteRequest::RestartAllContainers(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::RestartAllContainers(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RestartAllContainers\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PauseAllContainers(req) => {\n      let req = ExecuteRequest::PauseAllContainers(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PauseAllContainers(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PauseAllContainers\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::UnpauseAllContainers(req) => {\n      let req = ExecuteRequest::UnpauseAllContainers(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::UnpauseAllContainers(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at UnpauseAllContainers\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::StopAllContainers(req) => {\n      let req = ExecuteRequest::StopAllContainers(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::StopAllContainers(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at StopAllContainers\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PruneContainers(req) => {\n      let req = ExecuteRequest::PruneContainers(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PruneContainers(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PruneContainers\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::DeleteNetwork(req) => {\n      let req = ExecuteRequest::DeleteNetwork(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::DeleteNetwork(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at DeleteNetwork\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PruneNetworks(req) => {\n      let req = ExecuteRequest::PruneNetworks(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PruneNetworks(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PruneNetworks\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::DeleteImage(req) => {\n      let req = ExecuteRequest::DeleteImage(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::DeleteImage(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at DeleteImage\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PruneImages(req) => {\n      let req = ExecuteRequest::PruneImages(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PruneImages(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PruneImages\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::DeleteVolume(req) => {\n      let req = ExecuteRequest::DeleteVolume(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::DeleteVolume(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at DeleteVolume\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PruneVolumes(req) => {\n      let req = ExecuteRequest::PruneVolumes(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PruneVolumes(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PruneVolumes\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PruneDockerBuilders(req) => {\n      let req = ExecuteRequest::PruneDockerBuilders(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PruneDockerBuilders(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PruneDockerBuilders\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PruneBuildx(req) => {\n      let req = ExecuteRequest::PruneBuildx(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PruneBuildx(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PruneBuildx\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PruneSystem(req) => {\n      let req = ExecuteRequest::PruneSystem(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PruneSystem(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PruneSystem\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::RunSync(req) => {\n      let req = ExecuteRequest::RunSync(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::RunSync(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RunSync\"),\n        &update_id,\n      )\n      .await?\n    }\n    // Exception: This is a write operation.\n    Execution::CommitSync(req) => req\n      .resolve(&WriteArgs { user })\n      .await\n      .map_err(|e| e.error)\n      .context(\"Failed at CommitSync\")?,\n    Execution::DeployStack(req) => {\n      let req = ExecuteRequest::DeployStack(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::DeployStack(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at DeployStack\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchDeployStack(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchDeployStack not implemented correctly\"\n      ));\n    }\n    Execution::DeployStackIfChanged(req) => {\n      let req = ExecuteRequest::DeployStackIfChanged(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::DeployStackIfChanged(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at DeployStackIfChanged\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchDeployStackIfChanged(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchDeployStackIfChanged not implemented correctly\"\n      ));\n    }\n    Execution::PullStack(req) => {\n      let req = ExecuteRequest::PullStack(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PullStack(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PullStack\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchPullStack(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchPullStack not implemented correctly\"\n      ));\n    }\n    Execution::StartStack(req) => {\n      let req = ExecuteRequest::StartStack(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::StartStack(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at StartStack\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::RestartStack(req) => {\n      let req = ExecuteRequest::RestartStack(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::RestartStack(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RestartStack\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::PauseStack(req) => {\n      let req = ExecuteRequest::PauseStack(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::PauseStack(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at PauseStack\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::UnpauseStack(req) => {\n      let req = ExecuteRequest::UnpauseStack(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::UnpauseStack(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at UnpauseStack\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::StopStack(req) => {\n      let req = ExecuteRequest::StopStack(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::StopStack(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at StopStack\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::DestroyStack(req) => {\n      let req = ExecuteRequest::DestroyStack(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::DestroyStack(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at DestroyStack\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::RunStackService(req) => {\n      let req = ExecuteRequest::RunStackService(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::RunStackService(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at RunStackService\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BatchDestroyStack(_) => {\n      // All batch executions must be expanded in `execute_stage`\n      return Err(anyhow!(\n        \"Batch method BatchDestroyStack not implemented correctly\"\n      ));\n    }\n    Execution::TestAlerter(req) => {\n      let req = ExecuteRequest::TestAlerter(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::TestAlerter(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at TestAlerter\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::SendAlert(req) => {\n      let req = ExecuteRequest::SendAlert(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::SendAlert(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at SendAlert\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::ClearRepoCache(req) => {\n      let req = ExecuteRequest::ClearRepoCache(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::ClearRepoCache(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at ClearRepoCache\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::BackupCoreDatabase(req) => {\n      let req = ExecuteRequest::BackupCoreDatabase(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::BackupCoreDatabase(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at BackupCoreDatabase\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::GlobalAutoUpdate(req) => {\n      let req = ExecuteRequest::GlobalAutoUpdate(req);\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::GlobalAutoUpdate(req) = req else {\n        unreachable!()\n      };\n      let update_id = update.id.clone();\n      handle_resolve_result(\n        req\n          .resolve(&ExecuteArgs { user, update })\n          .await\n          .map_err(|e| e.error)\n          .context(\"Failed at GlobalAutoUpdate\"),\n        &update_id,\n      )\n      .await?\n    }\n    Execution::Sleep(req) => {\n      let duration = Duration::from_millis(req.duration_ms as u64);\n      tokio::time::sleep(duration).await;\n      Update {\n        success: true,\n        ..Default::default()\n      }\n    }\n  };\n  if update.success {\n    Ok(())\n  } else {\n    Err(anyhow!(\n      \"{}: execution not successful. see update '{}'\",\n      colored(\"ERROR\", Color::Red),\n      bold(&update.id),\n    ))\n  }\n}\n\n/// If the call to .resolve returns Err, the update may not be closed.\n/// This will ensure it is closed with error log attached.\nasync fn handle_resolve_result(\n  res: anyhow::Result<Update>,\n  update_id: &str,\n) -> anyhow::Result<Update> {\n  match res {\n    Ok(res) => Ok(res),\n    Err(e) => {\n      let log =\n        Log::error(\"execution error\", format_serror(&e.into()));\n      let mut update =\n        find_one_by_id(&db_client().updates, update_id)\n          .await\n          .context(\"Failed to query to db\")?\n          .context(\"no update exists with given id\")?;\n      update.logs.push(log);\n      update.finalize();\n      update_update(update.clone()).await?;\n      Ok(update)\n    }\n  }\n}\n\n/// ASSUMES FIRST LOG IS ALREADY CREATED\n#[instrument(level = \"debug\")]\nasync fn add_line_to_update(update: &Mutex<Update>, line: &str) {\n  let mut lock = update.lock().await;\n  let log = &mut lock.logs[0];\n  log.stdout.push('\\n');\n  log.stdout.push_str(line);\n  let update = lock.clone();\n  drop(lock);\n  if let Err(e) = update_update(update).await {\n    error!(\"Failed to update an update during procedure | {e:#}\");\n  };\n}\n\nasync fn extend_batch_exection<E: ExtendBatch>(\n  pattern: &str,\n  executions: &mut Vec<Execution>,\n) -> anyhow::Result<()> {\n  let more = list_full_for_user_using_pattern::<E::Resource>(\n    pattern,\n    Default::default(),\n    procedure_user(),\n    PermissionLevel::Read.into(),\n    &[],\n  )\n  .await?\n  .into_iter()\n  .map(|resource| E::single_execution(resource.name));\n  executions.extend(more);\n  Ok(())\n}\n\ntrait ExtendBatch {\n  type Resource: KomodoResource;\n  fn single_execution(name: String) -> Execution;\n}\n\nimpl ExtendBatch for BatchRunProcedure {\n  type Resource = Procedure;\n  fn single_execution(procedure: String) -> Execution {\n    Execution::RunProcedure(RunProcedure { procedure })\n  }\n}\n\nimpl ExtendBatch for BatchRunAction {\n  type Resource = Action;\n  fn single_execution(action: String) -> Execution {\n    Execution::RunAction(RunAction {\n      action,\n      args: Default::default(),\n    })\n  }\n}\n\nimpl ExtendBatch for BatchRunBuild {\n  type Resource = Build;\n  fn single_execution(build: String) -> Execution {\n    Execution::RunBuild(RunBuild { build })\n  }\n}\n\nimpl ExtendBatch for BatchCloneRepo {\n  type Resource = Repo;\n  fn single_execution(repo: String) -> Execution {\n    Execution::CloneRepo(CloneRepo { repo })\n  }\n}\n\nimpl ExtendBatch for BatchPullRepo {\n  type Resource = Repo;\n  fn single_execution(repo: String) -> Execution {\n    Execution::PullRepo(PullRepo { repo })\n  }\n}\n\nimpl ExtendBatch for BatchBuildRepo {\n  type Resource = Repo;\n  fn single_execution(repo: String) -> Execution {\n    Execution::BuildRepo(BuildRepo { repo })\n  }\n}\n\nimpl ExtendBatch for BatchDeploy {\n  type Resource = Deployment;\n  fn single_execution(deployment: String) -> Execution {\n    Execution::Deploy(Deploy {\n      deployment,\n      stop_signal: None,\n      stop_time: None,\n    })\n  }\n}\n\nimpl ExtendBatch for BatchDestroyDeployment {\n  type Resource = Deployment;\n  fn single_execution(deployment: String) -> Execution {\n    Execution::DestroyDeployment(DestroyDeployment {\n      deployment,\n      signal: None,\n      time: None,\n    })\n  }\n}\n\nimpl ExtendBatch for BatchDeployStack {\n  type Resource = Stack;\n  fn single_execution(stack: String) -> Execution {\n    Execution::DeployStack(DeployStack {\n      stack,\n      services: Vec::new(),\n      stop_time: None,\n    })\n  }\n}\n\nimpl ExtendBatch for BatchDeployStackIfChanged {\n  type Resource = Stack;\n  fn single_execution(stack: String) -> Execution {\n    Execution::DeployStackIfChanged(DeployStackIfChanged {\n      stack,\n      stop_time: None,\n    })\n  }\n}\n\nimpl ExtendBatch for BatchPullStack {\n  type Resource = Stack;\n  fn single_execution(stack: String) -> Execution {\n    Execution::PullStack(PullStack {\n      stack,\n      services: Vec::new(),\n    })\n  }\n}\n\nimpl ExtendBatch for BatchDestroyStack {\n  type Resource = Stack;\n  fn single_execution(stack: String) -> Execution {\n    Execution::DestroyStack(DestroyStack {\n      stack,\n      services: Vec::new(),\n      remove_orphans: false,\n      stop_time: None,\n    })\n  }\n}\n"
  },
  {
    "path": "bin/core/src/helpers/prune.rs",
    "content": "use anyhow::Context;\nuse async_timing_util::{\n  ONE_DAY_MS, Timelength, unix_timestamp_ms, wait_until_timelength,\n};\nuse database::mungos::{find::find_collect, mongodb::bson::doc};\nuse futures::{StreamExt, stream::FuturesUnordered};\nuse periphery_client::api::image::PruneImages;\n\nuse crate::{config::core_config, state::db_client};\n\nuse super::periphery_client;\n\npub fn spawn_prune_loop() {\n  tokio::spawn(async move {\n    loop {\n      wait_until_timelength(Timelength::OneDay, 5000).await;\n      let (images_res, stats_res, alerts_res) =\n        tokio::join!(prune_images(), prune_stats(), prune_alerts());\n      if let Err(e) = images_res {\n        error!(\"error in pruning images | {e:#}\");\n      }\n      if let Err(e) = stats_res {\n        error!(\"error in pruning stats | {e:#}\");\n      }\n      if let Err(e) = alerts_res {\n        error!(\"error in pruning alerts | {e:#}\");\n      }\n    }\n  });\n}\n\nasync fn prune_images() -> anyhow::Result<()> {\n  let mut futures = find_collect(\n    &db_client().servers,\n    doc! { \"config.enabled\": true, \"config.auto_prune\": true },\n    None,\n  )\n  .await\n  .context(\"failed to get servers from db\")?\n  .into_iter()\n  .map(|server| async move {\n    (\n      async {\n        periphery_client(&server)?.request(PruneImages {}).await\n      }\n      .await,\n      server,\n    )\n  })\n  .collect::<FuturesUnordered<_>>();\n\n  while let Some((res, server)) = futures.next().await {\n    if let Err(e) = res {\n      error!(\n        \"failed to prune images on server {} ({}) | {e:#}\",\n        server.name, server.id\n      )\n    }\n  }\n\n  Ok(())\n}\n\nasync fn prune_stats() -> anyhow::Result<()> {\n  if core_config().keep_stats_for_days == 0 {\n    return Ok(());\n  }\n  let delete_before_ts = (unix_timestamp_ms()\n    - core_config().keep_stats_for_days as u128 * ONE_DAY_MS)\n    as i64;\n  let res = db_client()\n    .stats\n    .delete_many(doc! {\n      \"ts\": { \"$lt\": delete_before_ts }\n    })\n    .await?;\n  if res.deleted_count > 0 {\n    info!(\"deleted {} stats from db\", res.deleted_count);\n  }\n  Ok(())\n}\n\nasync fn prune_alerts() -> anyhow::Result<()> {\n  if core_config().keep_alerts_for_days == 0 {\n    return Ok(());\n  }\n  let delete_before_ts = (unix_timestamp_ms()\n    - core_config().keep_alerts_for_days as u128 * ONE_DAY_MS)\n    as i64;\n  let res = db_client()\n    .alerts\n    .delete_many(doc! {\n      \"ts\": { \"$lt\": delete_before_ts }\n    })\n    .await?;\n  if res.deleted_count > 0 {\n    info!(\"deleted {} alerts from db\", res.deleted_count);\n  }\n  Ok(())\n}\n"
  },
  {
    "path": "bin/core/src/helpers/query.rs",
    "content": "use std::{\n  collections::HashMap,\n  str::FromStr,\n  sync::{Arc, OnceLock},\n};\n\nuse anyhow::{Context, anyhow};\nuse async_timing_util::{ONE_MIN_MS, unix_timestamp_ms};\nuse database::mungos::{\n  find::find_collect,\n  mongodb::{\n    bson::{Document, doc, oid::ObjectId},\n    options::FindOneOptions,\n  },\n};\nuse komodo_client::{\n  busy::Busy,\n  entities::{\n    Operation, ResourceTarget, ResourceTargetVariant,\n    action::{Action, ActionState},\n    alerter::Alerter,\n    build::Build,\n    builder::Builder,\n    deployment::{Deployment, DeploymentState},\n    docker::container::{\n      ContainerListItem, ContainerStateStatusEnum,\n    },\n    permission::{PermissionLevel, PermissionLevelAndSpecifics},\n    procedure::{Procedure, ProcedureState},\n    repo::Repo,\n    server::{Server, ServerState},\n    stack::{Stack, StackServiceNames, StackState},\n    stats::SystemInformation,\n    sync::ResourceSync,\n    tag::Tag,\n    update::Update,\n    user::{User, admin_service_user},\n    user_group::UserGroup,\n    variable::Variable,\n  },\n};\nuse periphery_client::api::stats;\nuse tokio::sync::Mutex;\n\nuse crate::{\n  config::core_config,\n  permission::get_user_permission_on_resource,\n  resource::{self, KomodoResource},\n  stack::compose_container_match_regex,\n  state::{\n    action_state_cache, action_states, db_client,\n    deployment_status_cache, procedure_state_cache,\n    stack_status_cache,\n  },\n};\n\nuse super::periphery_client;\n\n// user: Id or username\n#[instrument(level = \"debug\")]\npub async fn get_user(user: &str) -> anyhow::Result<User> {\n  if let Some(user) = admin_service_user(user) {\n    return Ok(user);\n  }\n  db_client()\n    .users\n    .find_one(id_or_username_filter(user))\n    .await\n    .context(\"failed to query mongo for user\")?\n    .with_context(|| format!(\"no user found with {user}\"))\n}\n\n#[instrument(level = \"debug\")]\npub async fn get_server_with_state(\n  server_id_or_name: &str,\n) -> anyhow::Result<(Server, ServerState)> {\n  let server = resource::get::<Server>(server_id_or_name).await?;\n  let state = get_server_state(&server).await;\n  Ok((server, state))\n}\n\n#[instrument(level = \"debug\")]\npub async fn get_server_state(server: &Server) -> ServerState {\n  if !server.config.enabled {\n    return ServerState::Disabled;\n  }\n  // Unwrap ok: Server disabled check above\n  match super::periphery_client(server)\n    .unwrap()\n    .request(periphery_client::api::GetHealth {})\n    .await\n  {\n    Ok(_) => ServerState::Ok,\n    Err(_) => ServerState::NotOk,\n  }\n}\n\n#[instrument(level = \"debug\")]\npub async fn get_deployment_state(\n  id: &String,\n) -> anyhow::Result<DeploymentState> {\n  if action_states()\n    .deployment\n    .get(id)\n    .await\n    .map(|s| s.get().map(|s| s.deploying))\n    .transpose()\n    .ok()\n    .flatten()\n    .unwrap_or_default()\n  {\n    return Ok(DeploymentState::Deploying);\n  }\n  let state = deployment_status_cache()\n    .get(id)\n    .await\n    .unwrap_or_default()\n    .curr\n    .state;\n  Ok(state)\n}\n\n/// Can pass all the containers from the same server\npub fn get_stack_state_from_containers(\n  ignore_services: &[String],\n  services: &[StackServiceNames],\n  containers: &[ContainerListItem],\n) -> StackState {\n  // first filter the containers to only ones which match the service\n  let services = services\n    .iter()\n    .filter(|service| {\n      !ignore_services.contains(&service.service_name)\n    })\n    .collect::<Vec<_>>();\n  let containers = containers.iter().filter(|container| {\n    services.iter().any(|StackServiceNames { service_name, container_name, .. }| {\n      match compose_container_match_regex(container_name)\n        .with_context(|| format!(\"failed to construct container name matching regex for service {service_name}\")) \n      {\n        Ok(regex) => regex,\n        Err(e) => {\n          warn!(\"{e:#}\");\n          return false\n        }\n      }.is_match(&container.name)\n    })\n  }).collect::<Vec<_>>();\n  if containers.is_empty() {\n    return StackState::Down;\n  }\n  if services.len() > containers.len() {\n    return StackState::Unhealthy;\n  }\n  let running = containers.iter().all(|container| {\n    container.state == ContainerStateStatusEnum::Running\n  });\n  if running {\n    return StackState::Running;\n  }\n  let paused = containers.iter().all(|container| {\n    container.state == ContainerStateStatusEnum::Paused\n  });\n  if paused {\n    return StackState::Paused;\n  }\n  let stopped = containers.iter().all(|container| {\n    container.state == ContainerStateStatusEnum::Exited\n  });\n  if stopped {\n    return StackState::Stopped;\n  }\n  let restarting = containers.iter().all(|container| {\n    container.state == ContainerStateStatusEnum::Restarting\n  });\n  if restarting {\n    return StackState::Restarting;\n  }\n  let dead = containers.iter().all(|container| {\n    container.state == ContainerStateStatusEnum::Dead\n  });\n  if dead {\n    return StackState::Dead;\n  }\n  let removing = containers.iter().all(|container| {\n    container.state == ContainerStateStatusEnum::Removing\n  });\n  if removing {\n    return StackState::Removing;\n  }\n  StackState::Unhealthy\n}\n\n#[instrument(level = \"debug\")]\npub async fn get_stack_state(\n  stack: &Stack,\n) -> anyhow::Result<StackState> {\n  if stack.config.server_id.is_empty() {\n    return Ok(StackState::Down);\n  }\n  let state = stack_status_cache()\n    .get(&stack.id)\n    .await\n    .unwrap_or_default()\n    .curr\n    .state;\n  Ok(state)\n}\n\n#[instrument(level = \"debug\")]\npub async fn get_tag(id_or_name: &str) -> anyhow::Result<Tag> {\n  let query = match ObjectId::from_str(id_or_name) {\n    Ok(id) => doc! { \"_id\": id },\n    Err(_) => doc! { \"name\": id_or_name },\n  };\n  db_client()\n    .tags\n    .find_one(query)\n    .await\n    .context(\"failed to query mongo for tag\")?\n    .with_context(|| format!(\"no tag found matching {id_or_name}\"))\n}\n\n#[instrument(level = \"debug\")]\npub async fn get_tag_check_owner(\n  id_or_name: &str,\n  user: &User,\n) -> anyhow::Result<Tag> {\n  let tag = get_tag(id_or_name).await?;\n  if user.admin || tag.owner == user.id {\n    return Ok(tag);\n  }\n  Err(anyhow!(\"user must be tag owner or admin\"))\n}\n\npub async fn get_all_tags(\n  filter: impl Into<Option<Document>>,\n) -> anyhow::Result<Vec<Tag>> {\n  find_collect(&db_client().tags, filter, None)\n    .await\n    .context(\"failed to query db for tags\")\n}\n\npub async fn get_id_to_tags(\n  filter: impl Into<Option<Document>>,\n) -> anyhow::Result<HashMap<String, Tag>> {\n  let res = find_collect(&db_client().tags, filter, None)\n    .await\n    .context(\"failed to query db for tags\")?\n    .into_iter()\n    .map(|tag| (tag.id.clone(), tag))\n    .collect();\n  Ok(res)\n}\n\n#[instrument(level = \"debug\")]\npub async fn get_user_user_groups(\n  user_id: &str,\n) -> anyhow::Result<Vec<UserGroup>> {\n  find_collect(\n    &db_client().user_groups,\n    doc! {\n      \"$or\": [\n        { \"everyone\": true },\n        { \"users\": user_id },\n      ]\n    },\n    None,\n  )\n  .await\n  .context(\"failed to query db for user groups\")\n}\n\n#[instrument(level = \"debug\")]\npub async fn get_user_user_group_ids(\n  user_id: &str,\n) -> anyhow::Result<Vec<String>> {\n  let res = get_user_user_groups(user_id)\n    .await?\n    .into_iter()\n    .map(|ug| ug.id)\n    .collect();\n  Ok(res)\n}\n\npub fn user_target_query(\n  user_id: &str,\n  user_groups: &[UserGroup],\n) -> anyhow::Result<Vec<Document>> {\n  let mut user_target_query = vec![\n    doc! { \"user_target.type\": \"User\", \"user_target.id\": user_id },\n  ];\n  let user_groups = user_groups.iter().map(|ug| {\n    doc! {\n      \"user_target.type\": \"UserGroup\", \"user_target.id\": &ug.id,\n    }\n  });\n  user_target_query.extend(user_groups);\n  Ok(user_target_query)\n}\n\npub async fn get_user_permission_on_target(\n  user: &User,\n  target: &ResourceTarget,\n) -> anyhow::Result<PermissionLevelAndSpecifics> {\n  match target {\n    ResourceTarget::System(_) => Ok(PermissionLevel::None.into()),\n    ResourceTarget::Build(id) => {\n      get_user_permission_on_resource::<Build>(user, id).await\n    }\n    ResourceTarget::Builder(id) => {\n      get_user_permission_on_resource::<Builder>(user, id).await\n    }\n    ResourceTarget::Deployment(id) => {\n      get_user_permission_on_resource::<Deployment>(user, id).await\n    }\n    ResourceTarget::Server(id) => {\n      get_user_permission_on_resource::<Server>(user, id).await\n    }\n    ResourceTarget::Repo(id) => {\n      get_user_permission_on_resource::<Repo>(user, id).await\n    }\n    ResourceTarget::Alerter(id) => {\n      get_user_permission_on_resource::<Alerter>(user, id).await\n    }\n    ResourceTarget::Procedure(id) => {\n      get_user_permission_on_resource::<Procedure>(user, id).await\n    }\n    ResourceTarget::Action(id) => {\n      get_user_permission_on_resource::<Action>(user, id).await\n    }\n    ResourceTarget::ResourceSync(id) => {\n      get_user_permission_on_resource::<ResourceSync>(user, id).await\n    }\n    ResourceTarget::Stack(id) => {\n      get_user_permission_on_resource::<Stack>(user, id).await\n    }\n  }\n}\n\npub fn id_or_name_filter(id_or_name: &str) -> Document {\n  match ObjectId::from_str(id_or_name) {\n    Ok(id) => doc! { \"_id\": id },\n    Err(_) => doc! { \"name\": id_or_name },\n  }\n}\n\npub fn id_or_username_filter(id_or_username: &str) -> Document {\n  match ObjectId::from_str(id_or_username) {\n    Ok(id) => doc! { \"_id\": id },\n    Err(_) => doc! { \"username\": id_or_username },\n  }\n}\n\npub async fn get_variable(name: &str) -> anyhow::Result<Variable> {\n  db_client()\n    .variables\n    .find_one(doc! { \"name\": &name })\n    .await\n    .context(\"failed at call to db\")?\n    .with_context(|| {\n      format!(\"no variable found with given name: {name}\")\n    })\n}\n\npub async fn get_latest_update(\n  resource_type: ResourceTargetVariant,\n  id: &str,\n  operation: Operation,\n) -> anyhow::Result<Option<Update>> {\n  db_client()\n    .updates\n    .find_one(doc! {\n      \"target.type\": resource_type.as_ref(),\n      \"target.id\": id,\n      \"operation\": operation.as_ref()\n    })\n    .with_options(\n      FindOneOptions::builder()\n        .sort(doc! { \"start_ts\": -1 })\n        .build(),\n    )\n    .await\n    .context(\"failed to query db for latest update\")\n}\n\npub struct VariablesAndSecrets {\n  pub variables: HashMap<String, String>,\n  pub secrets: HashMap<String, String>,\n}\n\npub async fn get_variables_and_secrets()\n-> anyhow::Result<VariablesAndSecrets> {\n  let variables = find_collect(&db_client().variables, None, None)\n    .await\n    .context(\"failed to get all variables from db\")?;\n  let mut secrets = core_config().secrets.clone();\n\n  // extend secrets with secret variables\n  secrets.extend(\n    variables.iter().filter(|variable| variable.is_secret).map(\n      |variable| (variable.name.clone(), variable.value.clone()),\n    ),\n  );\n\n  // collect non secret variables\n  let variables = variables\n    .into_iter()\n    .filter(|variable| !variable.is_secret)\n    .map(|variable| (variable.name, variable.value))\n    .collect();\n\n  Ok(VariablesAndSecrets { variables, secrets })\n}\n\n// This protects the peripheries from spam requests\nconst SYSTEM_INFO_EXPIRY: u128 = ONE_MIN_MS;\ntype SystemInfoCache =\n  Mutex<HashMap<String, Arc<(SystemInformation, u128)>>>;\nfn system_info_cache() -> &'static SystemInfoCache {\n  static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> =\n    OnceLock::new();\n  SYSTEM_INFO_CACHE.get_or_init(Default::default)\n}\n\npub async fn get_system_info(\n  server: &Server,\n) -> anyhow::Result<SystemInformation> {\n  let mut lock = system_info_cache().lock().await;\n  let res = match lock.get(&server.id) {\n    Some(cached) if cached.1 > unix_timestamp_ms() => {\n      cached.0.clone()\n    }\n    _ => {\n      let stats = periphery_client(server)?\n        .request(stats::GetSystemInformation {})\n        .await?;\n      lock.insert(\n        server.id.clone(),\n        (stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)\n          .into(),\n      );\n      stats\n    }\n  };\n  Ok(res)\n}\n\n/// Get last time procedure / action was run using Update query.\n/// Ignored whether run was successful.\npub async fn get_last_run_at<R: KomodoResource>(\n  id: &String,\n) -> anyhow::Result<Option<i64>> {\n  let resource_type = R::resource_type();\n  let res = db_client()\n    .updates\n    .find_one(doc! {\n      \"target.type\": resource_type.as_ref(),\n      \"target.id\": id,\n      \"operation\": format!(\"Run{resource_type}\"),\n      \"status\": \"Complete\"\n    })\n    .sort(doc! { \"start_ts\": -1 })\n    .await\n    .context(\"Failed to query updates collection for last run time\")?\n    .map(|u| u.start_ts);\n  Ok(res)\n}\n\npub async fn get_action_state(id: &String) -> ActionState {\n  if action_states()\n    .action\n    .get(id)\n    .await\n    .map(|s| s.get().map(|s| s.busy()))\n    .transpose()\n    .ok()\n    .flatten()\n    .unwrap_or_default()\n  {\n    return ActionState::Running;\n  }\n  action_state_cache().get(id).await.unwrap_or_default()\n}\n\npub async fn get_procedure_state(id: &String) -> ProcedureState {\n  if action_states()\n    .procedure\n    .get(id)\n    .await\n    .map(|s| s.get().map(|s| s.busy()))\n    .transpose()\n    .ok()\n    .flatten()\n    .unwrap_or_default()\n  {\n    return ProcedureState::Running;\n  }\n  procedure_state_cache().get(id).await.unwrap_or_default()\n}\n"
  },
  {
    "path": "bin/core/src/helpers/update.rs",
    "content": "use anyhow::Context;\nuse database::mungos::{\n  by_id::{find_one_by_id, update_one_by_id},\n  mongodb::bson::to_document,\n};\nuse komodo_client::entities::{\n  Operation, ResourceTarget,\n  action::Action,\n  alerter::Alerter,\n  build::Build,\n  deployment::Deployment,\n  komodo_timestamp,\n  procedure::Procedure,\n  repo::Repo,\n  server::Server,\n  stack::Stack,\n  sync::ResourceSync,\n  update::{Update, UpdateListItem},\n  user::User,\n};\n\nuse crate::{\n  api::execute::ExecuteRequest, resource, state::db_client,\n};\n\nuse super::channel::update_channel;\n\npub fn make_update(\n  target: impl Into<ResourceTarget>,\n  operation: Operation,\n  user: &User,\n) -> Update {\n  Update {\n    start_ts: komodo_timestamp(),\n    target: target.into(),\n    operation,\n    operator: user.id.clone(),\n    success: true,\n    ..Default::default()\n  }\n}\n\n#[instrument(level = \"debug\")]\npub async fn add_update(\n  mut update: Update,\n) -> anyhow::Result<String> {\n  update.id = db_client()\n    .updates\n    .insert_one(&update)\n    .await\n    .context(\"failed to insert update into db\")?\n    .inserted_id\n    .as_object_id()\n    .context(\"inserted_id is not object id\")?\n    .to_string();\n  let id = update.id.clone();\n  let update = update_list_item(update).await?;\n  let _ = send_update(update).await;\n  Ok(id)\n}\n\n#[instrument(level = \"debug\")]\npub async fn add_update_without_send(\n  update: &Update,\n) -> anyhow::Result<String> {\n  let id = db_client()\n    .updates\n    .insert_one(update)\n    .await\n    .context(\"failed to insert update into db\")?\n    .inserted_id\n    .as_object_id()\n    .context(\"inserted_id is not object id\")?\n    .to_string();\n  Ok(id)\n}\n\n#[instrument(level = \"debug\")]\npub async fn update_update(update: Update) -> anyhow::Result<()> {\n  update_one_by_id(&db_client().updates, &update.id, database::mungos::update::Update::Set(to_document(&update)?), None)\n    .await\n    .context(\"failed to update the update on db. the update build process was deleted\")?;\n  let update = update_list_item(update).await?;\n  let _ = send_update(update).await;\n  Ok(())\n}\n\n#[instrument(level = \"debug\")]\nasync fn update_list_item(\n  update: Update,\n) -> anyhow::Result<UpdateListItem> {\n  let username = if User::is_service_user(&update.operator) {\n    update.operator.clone()\n  } else {\n    find_one_by_id(&db_client().users, &update.operator)\n      .await\n      .context(\"failed to query mongo for user\")?\n      .with_context(|| {\n        format!(\"no user found with id {}\", update.operator)\n      })?\n      .username\n  };\n  let update = UpdateListItem {\n    id: update.id,\n    operation: update.operation,\n    start_ts: update.start_ts,\n    success: update.success,\n    operator: update.operator,\n    target: update.target,\n    status: update.status,\n    version: update.version,\n    other_data: update.other_data,\n    username,\n  };\n  Ok(update)\n}\n\n#[instrument(level = \"debug\")]\nasync fn send_update(update: UpdateListItem) -> anyhow::Result<()> {\n  update_channel().sender.lock().await.send(update)?;\n  Ok(())\n}\n\npub async fn init_execution_update(\n  request: &ExecuteRequest,\n  user: &User,\n) -> anyhow::Result<Update> {\n  let (operation, target) = match &request {\n    // Server\n    ExecuteRequest::StartContainer(data) => (\n      Operation::StartContainer,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::RestartContainer(data) => (\n      Operation::RestartContainer,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::PauseContainer(data) => (\n      Operation::PauseContainer,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::UnpauseContainer(data) => (\n      Operation::UnpauseContainer,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::StopContainer(data) => (\n      Operation::StopContainer,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::DestroyContainer(data) => (\n      Operation::DestroyContainer,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::StartAllContainers(data) => (\n      Operation::StartAllContainers,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::RestartAllContainers(data) => (\n      Operation::RestartAllContainers,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::PauseAllContainers(data) => (\n      Operation::PauseAllContainers,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::UnpauseAllContainers(data) => (\n      Operation::UnpauseAllContainers,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::StopAllContainers(data) => (\n      Operation::StopAllContainers,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::PruneContainers(data) => (\n      Operation::PruneContainers,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::DeleteNetwork(data) => (\n      Operation::DeleteNetwork,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::PruneNetworks(data) => (\n      Operation::PruneNetworks,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::DeleteImage(data) => (\n      Operation::DeleteImage,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::PruneImages(data) => (\n      Operation::PruneImages,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::DeleteVolume(data) => (\n      Operation::DeleteVolume,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::PruneVolumes(data) => (\n      Operation::PruneVolumes,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::PruneDockerBuilders(data) => (\n      Operation::PruneDockerBuilders,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::PruneBuildx(data) => (\n      Operation::PruneBuildx,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n    ExecuteRequest::PruneSystem(data) => (\n      Operation::PruneSystem,\n      ResourceTarget::Server(\n        resource::get::<Server>(&data.server).await?.id,\n      ),\n    ),\n\n    // Deployment\n    ExecuteRequest::Deploy(data) => (\n      Operation::Deploy,\n      ResourceTarget::Deployment(\n        resource::get::<Deployment>(&data.deployment).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchDeploy(_data) => {\n      return Ok(Default::default());\n    }\n    ExecuteRequest::PullDeployment(data) => (\n      Operation::PullDeployment,\n      ResourceTarget::Deployment(\n        resource::get::<Deployment>(&data.deployment).await?.id,\n      ),\n    ),\n    ExecuteRequest::StartDeployment(data) => (\n      Operation::StartDeployment,\n      ResourceTarget::Deployment(\n        resource::get::<Deployment>(&data.deployment).await?.id,\n      ),\n    ),\n    ExecuteRequest::RestartDeployment(data) => (\n      Operation::RestartDeployment,\n      ResourceTarget::Deployment(\n        resource::get::<Deployment>(&data.deployment).await?.id,\n      ),\n    ),\n    ExecuteRequest::PauseDeployment(data) => (\n      Operation::PauseDeployment,\n      ResourceTarget::Deployment(\n        resource::get::<Deployment>(&data.deployment).await?.id,\n      ),\n    ),\n    ExecuteRequest::UnpauseDeployment(data) => (\n      Operation::UnpauseDeployment,\n      ResourceTarget::Deployment(\n        resource::get::<Deployment>(&data.deployment).await?.id,\n      ),\n    ),\n    ExecuteRequest::StopDeployment(data) => (\n      Operation::StopDeployment,\n      ResourceTarget::Deployment(\n        resource::get::<Deployment>(&data.deployment).await?.id,\n      ),\n    ),\n    ExecuteRequest::DestroyDeployment(data) => (\n      Operation::DestroyDeployment,\n      ResourceTarget::Deployment(\n        resource::get::<Deployment>(&data.deployment).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchDestroyDeployment(_data) => {\n      return Ok(Default::default());\n    }\n\n    // Build\n    ExecuteRequest::RunBuild(data) => (\n      Operation::RunBuild,\n      ResourceTarget::Build(\n        resource::get::<Build>(&data.build).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchRunBuild(_data) => {\n      return Ok(Default::default());\n    }\n    ExecuteRequest::CancelBuild(data) => (\n      Operation::CancelBuild,\n      ResourceTarget::Build(\n        resource::get::<Build>(&data.build).await?.id,\n      ),\n    ),\n\n    // Repo\n    ExecuteRequest::CloneRepo(data) => (\n      Operation::CloneRepo,\n      ResourceTarget::Repo(\n        resource::get::<Repo>(&data.repo).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchCloneRepo(_data) => {\n      return Ok(Default::default());\n    }\n    ExecuteRequest::PullRepo(data) => (\n      Operation::PullRepo,\n      ResourceTarget::Repo(\n        resource::get::<Repo>(&data.repo).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchPullRepo(_data) => {\n      return Ok(Default::default());\n    }\n    ExecuteRequest::BuildRepo(data) => (\n      Operation::BuildRepo,\n      ResourceTarget::Repo(\n        resource::get::<Repo>(&data.repo).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchBuildRepo(_data) => {\n      return Ok(Default::default());\n    }\n    ExecuteRequest::CancelRepoBuild(data) => (\n      Operation::CancelRepoBuild,\n      ResourceTarget::Repo(\n        resource::get::<Repo>(&data.repo).await?.id,\n      ),\n    ),\n\n    // Procedure\n    ExecuteRequest::RunProcedure(data) => (\n      Operation::RunProcedure,\n      ResourceTarget::Procedure(\n        resource::get::<Procedure>(&data.procedure).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchRunProcedure(_) => {\n      return Ok(Default::default());\n    }\n\n    // Action\n    ExecuteRequest::RunAction(data) => (\n      Operation::RunAction,\n      ResourceTarget::Action(\n        resource::get::<Action>(&data.action).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchRunAction(_) => {\n      return Ok(Default::default());\n    }\n\n    // Resource Sync\n    ExecuteRequest::RunSync(data) => (\n      Operation::RunSync,\n      ResourceTarget::ResourceSync(\n        resource::get::<ResourceSync>(&data.sync).await?.id,\n      ),\n    ),\n\n    // Stack\n    ExecuteRequest::DeployStack(data) => (\n      if !data.services.is_empty() {\n        Operation::DeployStackService\n      } else {\n        Operation::DeployStack\n      },\n      ResourceTarget::Stack(\n        resource::get::<Stack>(&data.stack).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchDeployStack(_data) => {\n      return Ok(Default::default());\n    }\n    ExecuteRequest::DeployStackIfChanged(data) => (\n      Operation::DeployStack,\n      ResourceTarget::Stack(\n        resource::get::<Stack>(&data.stack).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchDeployStackIfChanged(_data) => {\n      return Ok(Default::default());\n    }\n    ExecuteRequest::StartStack(data) => (\n      if !data.services.is_empty() {\n        Operation::StartStackService\n      } else {\n        Operation::StartStack\n      },\n      ResourceTarget::Stack(\n        resource::get::<Stack>(&data.stack).await?.id,\n      ),\n    ),\n    ExecuteRequest::PullStack(data) => (\n      if !data.services.is_empty() {\n        Operation::PullStackService\n      } else {\n        Operation::PullStack\n      },\n      ResourceTarget::Stack(\n        resource::get::<Stack>(&data.stack).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchPullStack(_data) => {\n      return Ok(Default::default());\n    }\n    ExecuteRequest::RestartStack(data) => (\n      if !data.services.is_empty() {\n        Operation::RestartStackService\n      } else {\n        Operation::RestartStack\n      },\n      ResourceTarget::Stack(\n        resource::get::<Stack>(&data.stack).await?.id,\n      ),\n    ),\n    ExecuteRequest::PauseStack(data) => (\n      if !data.services.is_empty() {\n        Operation::PauseStackService\n      } else {\n        Operation::PauseStack\n      },\n      ResourceTarget::Stack(\n        resource::get::<Stack>(&data.stack).await?.id,\n      ),\n    ),\n    ExecuteRequest::UnpauseStack(data) => (\n      if !data.services.is_empty() {\n        Operation::UnpauseStackService\n      } else {\n        Operation::UnpauseStack\n      },\n      ResourceTarget::Stack(\n        resource::get::<Stack>(&data.stack).await?.id,\n      ),\n    ),\n    ExecuteRequest::StopStack(data) => (\n      if !data.services.is_empty() {\n        Operation::StopStackService\n      } else {\n        Operation::StopStack\n      },\n      ResourceTarget::Stack(\n        resource::get::<Stack>(&data.stack).await?.id,\n      ),\n    ),\n    ExecuteRequest::DestroyStack(data) => (\n      if !data.services.is_empty() {\n        Operation::DestroyStackService\n      } else {\n        Operation::DestroyStack\n      },\n      ResourceTarget::Stack(\n        resource::get::<Stack>(&data.stack).await?.id,\n      ),\n    ),\n    ExecuteRequest::BatchDestroyStack(_data) => {\n      return Ok(Default::default());\n    }\n\n    ExecuteRequest::RunStackService(data) => (\n      Operation::RunStackService,\n      ResourceTarget::Stack(\n        resource::get::<Stack>(&data.stack).await?.id,\n      ),\n    ),\n\n    // Alerter\n    ExecuteRequest::TestAlerter(data) => (\n      Operation::TestAlerter,\n      ResourceTarget::Alerter(\n        resource::get::<Alerter>(&data.alerter).await?.id,\n      ),\n    ),\n    ExecuteRequest::SendAlert(_) => {\n      (Operation::SendAlert, ResourceTarget::system())\n    }\n\n    // Maintenance\n    ExecuteRequest::ClearRepoCache(_data) => {\n      (Operation::ClearRepoCache, ResourceTarget::system())\n    }\n    ExecuteRequest::BackupCoreDatabase(_data) => {\n      (Operation::BackupCoreDatabase, ResourceTarget::system())\n    }\n    ExecuteRequest::GlobalAutoUpdate(_data) => {\n      (Operation::GlobalAutoUpdate, ResourceTarget::system())\n    }\n  };\n\n  let mut update = make_update(target, operation, user);\n  update.in_progress();\n\n  // Hold off on even adding update for DeployStackIfChanged\n  if !matches!(&request, ExecuteRequest::DeployStackIfChanged(_)) {\n    // Don't actually send it here, let the handlers send it after they can set action state.\n    update.id = add_update_without_send(&update).await?;\n  }\n\n  Ok(update)\n}\n"
  },
  {
    "path": "bin/core/src/listener/integrations/github.rs",
    "content": "use anyhow::{Context, anyhow};\nuse axum::http::HeaderMap;\nuse hex::ToHex;\nuse hmac::{Hmac, Mac};\nuse serde::Deserialize;\nuse sha2::Sha256;\n\nuse crate::{\n  config::core_config,\n  listener::{ExtractBranch, VerifySecret},\n};\n\ntype HmacSha256 = Hmac<Sha256>;\n\n/// Listener implementation for Github type API, including Gitea\npub struct Github;\n\nimpl VerifySecret for Github {\n  #[instrument(\"VerifyGithubSecret\", skip_all)]\n  fn verify_secret(\n    headers: HeaderMap,\n    body: &str,\n    custom_secret: &str,\n  ) -> anyhow::Result<()> {\n    let signature = headers\n      .get(\"x-hub-signature-256\")\n      .context(\"No github signature in headers\")?;\n    let signature = signature\n      .to_str()\n      .context(\"Failed to get signature as string\")?;\n    let signature =\n      signature.strip_prefix(\"sha256=\").unwrap_or(signature);\n    let secret_bytes = if custom_secret.is_empty() {\n      core_config().webhook_secret.as_bytes()\n    } else {\n      custom_secret.as_bytes()\n    };\n    let mut mac = HmacSha256::new_from_slice(secret_bytes)\n      .context(\"Failed to create hmac sha256 from secret\")?;\n    mac.update(body.as_bytes());\n    let expected = mac.finalize().into_bytes().encode_hex::<String>();\n    if signature == expected {\n      Ok(())\n    } else {\n      Err(anyhow!(\"Signature does not equal expected\"))\n    }\n  }\n}\n\n#[derive(Deserialize)]\nstruct GithubWebhookBody {\n  #[serde(rename = \"ref\")]\n  branch: String,\n}\n\nimpl ExtractBranch for Github {\n  fn extract_branch(body: &str) -> anyhow::Result<String> {\n    let branch = serde_json::from_str::<GithubWebhookBody>(body)\n      .context(\"Failed to parse github request body\")?\n      .branch\n      .replace(\"refs/heads/\", \"\");\n    Ok(branch)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/listener/integrations/gitlab.rs",
    "content": "use anyhow::{Context, anyhow};\nuse serde::Deserialize;\n\nuse crate::{\n  config::core_config,\n  listener::{ExtractBranch, VerifySecret},\n};\n\n/// Listener implementation for Gitlab type API\npub struct Gitlab;\n\nimpl VerifySecret for Gitlab {\n  #[instrument(\"VerifyGitlabSecret\", skip_all)]\n  fn verify_secret(\n    headers: axum::http::HeaderMap,\n    _body: &str,\n    custom_secret: &str,\n  ) -> anyhow::Result<()> {\n    let token = headers\n      .get(\"x-gitlab-token\")\n      .context(\"No gitlab token in headers\")?;\n    let token =\n      token.to_str().context(\"Failed to get token as string\")?;\n    let secret = if custom_secret.is_empty() {\n      core_config().webhook_secret.as_str()\n    } else {\n      custom_secret\n    };\n    if token == secret {\n      Ok(())\n    } else {\n      Err(anyhow!(\"Webhook secret does not match expected.\"))\n    }\n  }\n}\n\n#[derive(Deserialize)]\nstruct GitlabWebhookBody {\n  #[serde(rename = \"ref\")]\n  branch: String,\n}\n\nimpl ExtractBranch for Gitlab {\n  fn extract_branch(body: &str) -> anyhow::Result<String> {\n    let branch = serde_json::from_str::<GitlabWebhookBody>(body)\n      .context(\"Failed to parse gitlab request body\")?\n      .branch\n      .replace(\"refs/heads/\", \"\");\n    Ok(branch)\n  }\n}\n"
  },
  {
    "path": "bin/core/src/listener/integrations/mod.rs",
    "content": "pub mod github;\npub mod gitlab;\n"
  },
  {
    "path": "bin/core/src/listener/mod.rs",
    "content": "use std::sync::Arc;\n\nuse anyhow::anyhow;\nuse axum::{Router, http::HeaderMap};\nuse komodo_client::entities::resource::Resource;\nuse tokio::sync::Mutex;\n\nuse crate::{helpers::cache::Cache, resource::KomodoResource};\n\nmod integrations;\nmod resources;\nmod router;\n\nuse integrations::*;\n\npub fn router() -> Router {\n  Router::new()\n    .nest(\"/github\", router::router::<github::Github>())\n    .nest(\"/gitlab\", router::router::<gitlab::Gitlab>())\n}\n\ntype ListenerLockCache = Cache<String, Arc<Mutex<()>>>;\n\n/// Implemented for all resources which can recieve webhook.\ntrait CustomSecret: KomodoResource {\n  fn custom_secret(\n    resource: &Resource<Self::Config, Self::Info>,\n  ) -> &str;\n}\n\n/// Implemented on the integration struct, eg [integrations::github::Github]\ntrait VerifySecret {\n  fn verify_secret(\n    headers: HeaderMap,\n    body: &str,\n    custom_secret: &str,\n  ) -> anyhow::Result<()>;\n}\n\n/// Implemented on the integration struct, eg [integrations::github::Github]\ntrait ExtractBranch {\n  fn extract_branch(body: &str) -> anyhow::Result<String>;\n  fn verify_branch(body: &str, expected: &str) -> anyhow::Result<()> {\n    let branch = Self::extract_branch(body)?;\n    if branch == expected {\n      Ok(())\n    } else {\n      Err(anyhow!(\"request branch does not match expected\"))\n    }\n  }\n}\n\n/// For Procedures and Actions, incoming webhook\n/// can be triggered by any branch by using `__ANY__`\n/// as the branch in the webhook URL.\nconst ANY_BRANCH: &str = \"__ANY__\";\n"
  },
  {
    "path": "bin/core/src/listener/resources.rs",
    "content": "use std::{str::FromStr, sync::OnceLock};\n\nuse anyhow::{Context, anyhow};\nuse komodo_client::{\n  api::{\n    execute::*,\n    write::{RefreshResourceSyncPending, RefreshStackCache},\n  },\n  entities::{\n    action::Action, build::Build, procedure::Procedure, repo::Repo,\n    stack::Stack, sync::ResourceSync, user::git_webhook_user,\n  },\n};\nuse resolver_api::Resolve;\nuse serde::Deserialize;\nuse serde_json::json;\n\nuse crate::{\n  api::{\n    execute::{ExecuteArgs, ExecuteRequest},\n    write::WriteArgs,\n  },\n  helpers::update::init_execution_update,\n};\n\nuse super::{ANY_BRANCH, ListenerLockCache};\n\n// =======\n//  BUILD\n// =======\n\nimpl super::CustomSecret for Build {\n  fn custom_secret(resource: &Self) -> &str {\n    &resource.config.webhook_secret\n  }\n}\n\nfn build_locks() -> &'static ListenerLockCache {\n  static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();\n  BUILD_LOCKS.get_or_init(Default::default)\n}\n\npub async fn handle_build_webhook<B: super::ExtractBranch>(\n  build: Build,\n  body: String,\n) -> anyhow::Result<()> {\n  if !build.config.webhook_enabled {\n    return Ok(());\n  }\n\n  // Acquire and hold lock to make a task queue for\n  // subsequent listener calls on same resource.\n  // It would fail if we let it go through from action state busy.\n  let lock = build_locks().get_or_insert_default(&build.id).await;\n  let _lock = lock.lock().await;\n\n  B::verify_branch(&body, &build.config.branch)?;\n\n  let user = git_webhook_user().to_owned();\n  let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });\n  let update = init_execution_update(&req, &user).await?;\n  let ExecuteRequest::RunBuild(req) = req else {\n    unreachable!()\n  };\n  req\n    .resolve(&ExecuteArgs { user, update })\n    .await\n    .map_err(|e| e.error)?;\n  Ok(())\n}\n\n// ======\n//  REPO\n// ======\n\nimpl super::CustomSecret for Repo {\n  fn custom_secret(resource: &Self) -> &str {\n    &resource.config.webhook_secret\n  }\n}\n\nfn repo_locks() -> &'static ListenerLockCache {\n  static REPO_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();\n  REPO_LOCKS.get_or_init(Default::default)\n}\n\npub trait RepoExecution {\n  async fn resolve(repo: Repo) -> anyhow::Result<()>;\n}\n\nimpl RepoExecution for CloneRepo {\n  async fn resolve(repo: Repo) -> anyhow::Result<()> {\n    let user = git_webhook_user().to_owned();\n    let req =\n      crate::api::execute::ExecuteRequest::CloneRepo(CloneRepo {\n        repo: repo.id,\n      });\n    let update = init_execution_update(&req, &user).await?;\n    let crate::api::execute::ExecuteRequest::CloneRepo(req) = req\n    else {\n      unreachable!()\n    };\n    req\n      .resolve(&ExecuteArgs { user, update })\n      .await\n      .map_err(|e| e.error)?;\n    Ok(())\n  }\n}\n\nimpl RepoExecution for PullRepo {\n  async fn resolve(repo: Repo) -> anyhow::Result<()> {\n    let user = git_webhook_user().to_owned();\n    let req =\n      crate::api::execute::ExecuteRequest::PullRepo(PullRepo {\n        repo: repo.id,\n      });\n    let update = init_execution_update(&req, &user).await?;\n    let crate::api::execute::ExecuteRequest::PullRepo(req) = req\n    else {\n      unreachable!()\n    };\n    req\n      .resolve(&ExecuteArgs { user, update })\n      .await\n      .map_err(|e| e.error)?;\n    Ok(())\n  }\n}\n\nimpl RepoExecution for BuildRepo {\n  async fn resolve(repo: Repo) -> anyhow::Result<()> {\n    let user = git_webhook_user().to_owned();\n    let req =\n      crate::api::execute::ExecuteRequest::BuildRepo(BuildRepo {\n        repo: repo.id,\n      });\n    let update = init_execution_update(&req, &user).await?;\n    let crate::api::execute::ExecuteRequest::BuildRepo(req) = req\n    else {\n      unreachable!()\n    };\n    req\n      .resolve(&ExecuteArgs { user, update })\n      .await\n      .map_err(|e| e.error)?;\n    Ok(())\n  }\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum RepoWebhookOption {\n  Clone,\n  Pull,\n  Build,\n}\n\npub async fn handle_repo_webhook<B: super::ExtractBranch>(\n  option: RepoWebhookOption,\n  repo: Repo,\n  body: String,\n) -> anyhow::Result<()> {\n  match option {\n    RepoWebhookOption::Clone => {\n      handle_repo_webhook_inner::<B, CloneRepo>(repo, body).await\n    }\n    RepoWebhookOption::Pull => {\n      handle_repo_webhook_inner::<B, PullRepo>(repo, body).await\n    }\n    RepoWebhookOption::Build => {\n      handle_repo_webhook_inner::<B, BuildRepo>(repo, body).await\n    }\n  }\n}\n\nasync fn handle_repo_webhook_inner<\n  B: super::ExtractBranch,\n  E: RepoExecution,\n>(\n  repo: Repo,\n  body: String,\n) -> anyhow::Result<()> {\n  if !repo.config.webhook_enabled {\n    return Ok(());\n  }\n\n  // Acquire and hold lock to make a task queue for\n  // subsequent listener calls on same resource.\n  // It would fail if we let it go through from action state busy.\n  let lock = repo_locks().get_or_insert_default(&repo.id).await;\n  let _lock = lock.lock().await;\n\n  B::verify_branch(&body, &repo.config.branch)?;\n\n  E::resolve(repo).await\n}\n\n// =======\n//  STACK\n// =======\n\nimpl super::CustomSecret for Stack {\n  fn custom_secret(resource: &Self) -> &str {\n    &resource.config.webhook_secret\n  }\n}\n\nfn stack_locks() -> &'static ListenerLockCache {\n  static STACK_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();\n  STACK_LOCKS.get_or_init(Default::default)\n}\n\npub trait StackExecution {\n  async fn resolve(stack: Stack) -> serror::Result<()>;\n}\n\nimpl StackExecution for RefreshStackCache {\n  async fn resolve(stack: Stack) -> serror::Result<()> {\n    RefreshStackCache { stack: stack.id }\n      .resolve(&WriteArgs {\n        user: git_webhook_user().to_owned(),\n      })\n      .await?;\n    Ok(())\n  }\n}\n\nimpl StackExecution for DeployStack {\n  async fn resolve(stack: Stack) -> serror::Result<()> {\n    let user = git_webhook_user().to_owned();\n    if stack.config.webhook_force_deploy {\n      let req = ExecuteRequest::DeployStack(DeployStack {\n        stack: stack.id,\n        services: Vec::new(),\n        stop_time: None,\n      });\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::DeployStack(req) = req else {\n        unreachable!()\n      };\n      req\n        .resolve(&ExecuteArgs { user, update })\n        .await\n        .map_err(|e| e.error)?;\n    } else {\n      let req =\n        ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {\n          stack: stack.id,\n          stop_time: None,\n        });\n      let update = init_execution_update(&req, &user).await?;\n      let ExecuteRequest::DeployStackIfChanged(req) = req else {\n        unreachable!()\n      };\n      req\n        .resolve(&ExecuteArgs { user, update })\n        .await\n        .map_err(|e| e.error)?;\n    }\n\n    Ok(())\n  }\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum StackWebhookOption {\n  Refresh,\n  Deploy,\n}\n\npub async fn handle_stack_webhook<B: super::ExtractBranch>(\n  option: StackWebhookOption,\n  stack: Stack,\n  body: String,\n) -> anyhow::Result<()> {\n  match option {\n    StackWebhookOption::Refresh => {\n      handle_stack_webhook_inner::<B, RefreshStackCache>(stack, body)\n        .await\n    }\n    StackWebhookOption::Deploy => {\n      handle_stack_webhook_inner::<B, DeployStack>(stack, body).await\n    }\n  }\n}\n\npub async fn handle_stack_webhook_inner<\n  B: super::ExtractBranch,\n  E: StackExecution,\n>(\n  stack: Stack,\n  body: String,\n) -> anyhow::Result<()> {\n  if !stack.config.webhook_enabled {\n    return Ok(());\n  }\n\n  // Acquire and hold lock to make a task queue for\n  // subsequent listener calls on same resource.\n  // It would fail if we let it go through, from \"action state busy\".\n  let lock = stack_locks().get_or_insert_default(&stack.id).await;\n  let _lock = lock.lock().await;\n\n  B::verify_branch(&body, &stack.config.branch)?;\n\n  E::resolve(stack).await.map_err(|e| e.error)\n}\n\n// ======\n//  SYNC\n// ======\n\nimpl super::CustomSecret for ResourceSync {\n  fn custom_secret(resource: &Self) -> &str {\n    &resource.config.webhook_secret\n  }\n}\n\nfn sync_locks() -> &'static ListenerLockCache {\n  static SYNC_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();\n  SYNC_LOCKS.get_or_init(Default::default)\n}\n\npub trait SyncExecution {\n  async fn resolve(sync: ResourceSync) -> anyhow::Result<()>;\n}\n\nimpl SyncExecution for RefreshResourceSyncPending {\n  async fn resolve(sync: ResourceSync) -> anyhow::Result<()> {\n    RefreshResourceSyncPending { sync: sync.id }\n      .resolve(&WriteArgs {\n        user: git_webhook_user().to_owned(),\n      })\n      .await\n      .map_err(|e| e.error)?;\n    Ok(())\n  }\n}\n\nimpl SyncExecution for RunSync {\n  async fn resolve(sync: ResourceSync) -> anyhow::Result<()> {\n    let user = git_webhook_user().to_owned();\n    let req = ExecuteRequest::RunSync(RunSync {\n      sync: sync.id,\n      resource_type: None,\n      resources: None,\n    });\n    let update = init_execution_update(&req, &user).await?;\n    let ExecuteRequest::RunSync(req) = req else {\n      unreachable!()\n    };\n    req\n      .resolve(&ExecuteArgs { user, update })\n      .await\n      .map_err(|e| e.error)?;\n    Ok(())\n  }\n}\n\n#[derive(Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum SyncWebhookOption {\n  Refresh,\n  Sync,\n}\n\npub async fn handle_sync_webhook<B: super::ExtractBranch>(\n  option: SyncWebhookOption,\n  sync: ResourceSync,\n  body: String,\n) -> anyhow::Result<()> {\n  match option {\n    SyncWebhookOption::Refresh => {\n      handle_sync_webhook_inner::<B, RefreshResourceSyncPending>(\n        sync, body,\n      )\n      .await\n    }\n    SyncWebhookOption::Sync => {\n      handle_sync_webhook_inner::<B, RunSync>(sync, body).await\n    }\n  }\n}\n\nasync fn handle_sync_webhook_inner<\n  B: super::ExtractBranch,\n  E: SyncExecution,\n>(\n  sync: ResourceSync,\n  body: String,\n) -> anyhow::Result<()> {\n  if !sync.config.webhook_enabled {\n    return Ok(());\n  }\n\n  // Acquire and hold lock to make a task queue for\n  // subsequent listener calls on same resource.\n  // It would fail if we let it go through from action state busy.\n  let lock = sync_locks().get_or_insert_default(&sync.id).await;\n  let _lock = lock.lock().await;\n\n  B::verify_branch(&body, &sync.config.branch)?;\n\n  E::resolve(sync).await\n}\n\n// ===========\n//  PROCEDURE\n// ===========\n\nimpl super::CustomSecret for Procedure {\n  fn custom_secret(resource: &Self) -> &str {\n    &resource.config.webhook_secret\n  }\n}\n\nfn procedure_locks() -> &'static ListenerLockCache {\n  static PROCEDURE_LOCKS: OnceLock<ListenerLockCache> =\n    OnceLock::new();\n  PROCEDURE_LOCKS.get_or_init(Default::default)\n}\n\npub async fn handle_procedure_webhook<B: super::ExtractBranch>(\n  procedure: Procedure,\n  target_branch: &str,\n  body: String,\n) -> anyhow::Result<()> {\n  if !procedure.config.webhook_enabled {\n    return Ok(());\n  }\n\n  // Acquire and hold lock to make a task queue for\n  // subsequent listener calls on same resource.\n  // It would fail if we let it go through from action state busy.\n  let lock =\n    procedure_locks().get_or_insert_default(&procedure.id).await;\n  let _lock = lock.lock().await;\n\n  if target_branch != ANY_BRANCH {\n    B::verify_branch(&body, target_branch)?;\n  }\n\n  let user = git_webhook_user().to_owned();\n  let req = ExecuteRequest::RunProcedure(RunProcedure {\n    procedure: procedure.id,\n  });\n  let update = init_execution_update(&req, &user).await?;\n  let ExecuteRequest::RunProcedure(req) = req else {\n    unreachable!()\n  };\n  req\n    .resolve(&ExecuteArgs { user, update })\n    .await\n    .map_err(|e| e.error)?;\n  Ok(())\n}\n\n// ========\n//  ACTION\n// ========\n\nimpl super::CustomSecret for Action {\n  fn custom_secret(resource: &Self) -> &str {\n    &resource.config.webhook_secret\n  }\n}\n\nfn action_locks() -> &'static ListenerLockCache {\n  static ACTION_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();\n  ACTION_LOCKS.get_or_init(Default::default)\n}\n\npub async fn handle_action_webhook<B: super::ExtractBranch>(\n  action: Action,\n  target_branch: &str,\n  body: String,\n) -> anyhow::Result<()> {\n  if !action.config.webhook_enabled {\n    return Ok(());\n  }\n\n  // Acquire and hold lock to make a task queue for\n  // subsequent listener calls on same resource.\n  // It would fail if we let it go through from action state busy.\n  let lock = action_locks().get_or_insert_default(&action.id).await;\n  let _lock = lock.lock().await;\n\n  let branch = B::extract_branch(&body)?;\n\n  if target_branch != ANY_BRANCH && branch != target_branch {\n    return Err(anyhow!(\"request branch does not match expected\"));\n  }\n\n  let user = git_webhook_user().to_owned();\n\n  let body = serde_json::Value::from_str(&body)\n    .context(\"Failed to deserialize webhook body\")?;\n  let serde_json::Value::Object(args) = json!({\n    \"WEBHOOK_BRANCH\": branch,\n    \"WEBHOOK_BODY\": body,\n  }) else {\n    return Err(anyhow!(\"Something is wrong with serde_json...\"));\n  };\n\n  let req = ExecuteRequest::RunAction(RunAction {\n    action: action.id,\n    args: args.into(),\n  });\n  let update = init_execution_update(&req, &user).await?;\n  let ExecuteRequest::RunAction(req) = req else {\n    unreachable!()\n  };\n  req\n    .resolve(&ExecuteArgs { user, update })\n    .await\n    .map_err(|e| e.error)?;\n  Ok(())\n}\n"
  },
  {
    "path": "bin/core/src/listener/router.rs",
    "content": "use axum::{Router, extract::Path, http::HeaderMap, routing::post};\nuse komodo_client::entities::{\n  action::Action, build::Build, procedure::Procedure, repo::Repo,\n  resource::Resource, stack::Stack, sync::ResourceSync,\n};\nuse reqwest::StatusCode;\nuse serde::Deserialize;\nuse serror::AddStatusCode;\nuse tracing::Instrument;\n\nuse crate::resource::KomodoResource;\n\nuse super::{\n  CustomSecret, ExtractBranch, VerifySecret,\n  resources::{\n    RepoWebhookOption, StackWebhookOption, SyncWebhookOption,\n    handle_action_webhook, handle_build_webhook,\n    handle_procedure_webhook, handle_repo_webhook,\n    handle_stack_webhook, handle_sync_webhook,\n  },\n};\n\n#[derive(Deserialize)]\nstruct Id {\n  id: String,\n}\n\n#[derive(Deserialize)]\nstruct IdAndOption<T> {\n  id: String,\n  option: T,\n}\n\n#[derive(Deserialize)]\nstruct IdAndBranch {\n  id: String,\n  #[serde(default = \"default_branch\")]\n  branch: String,\n}\n\nfn default_branch() -> String {\n  String::from(\"main\")\n}\n\npub fn router<P: VerifySecret + ExtractBranch>() -> Router {\n  Router::new()\n  .route(\n    \"/build/{id}\",\n    post(\n      |Path(Id { id }), headers: HeaderMap, body: String| async move {\n        let build =\n          auth_webhook::<P, Build>(&id, headers, &body).await?;\n        tokio::spawn(async move {\n          let span = info_span!(\"BuildWebhook\", id);\n          async {\n            let res = handle_build_webhook::<P>(\n              build, body,\n            )\n            .await;\n            if let Err(e) = res {\n              warn!(\n                \"Failed at running webhook for build {id} | {e:#}\"\n              );\n            }\n          }\n          .instrument(span)\n          .await\n        });\n        serror::Result::Ok(())\n      },\n    ),\n  )\n  .route(\n    \"/repo/{id}/{option}\",\n    post(\n      |Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {\n        let repo =\n          auth_webhook::<P, Repo>(&id, headers, &body).await?;\n        tokio::spawn(async move {\n          let span = info_span!(\"RepoWebhook\", id);\n          async {\n            let res = handle_repo_webhook::<P>(\n              option, repo, body,\n            )\n            .await;\n            if let Err(e) = res {\n              warn!(\n                \"Failed at running webhook for repo {id} | {e:#}\"\n              );\n            }\n          }\n          .instrument(span)\n          .await\n        });\n        serror::Result::Ok(())\n      },\n    ),\n  )\n  .route(\n    \"/stack/{id}/{option}\",\n    post(\n      |Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {\n        let stack =\n          auth_webhook::<P, Stack>(&id, headers, &body).await?;\n        tokio::spawn(async move {\n          let span = info_span!(\"StackWebhook\", id);\n          async {\n            let res = handle_stack_webhook::<P>(\n              option, stack, body,\n            )\n            .await;\n            if let Err(e) = res {\n              warn!(\n                \"Failed at running webhook for stack {id} | {e:#}\"\n              );\n            }\n          }\n          .instrument(span)\n          .await\n        });\n        serror::Result::Ok(())\n      },\n    ),\n  )\n  .route(\n    \"/sync/{id}/{option}\",\n    post(\n      |Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {\n        let sync =\n          auth_webhook::<P, ResourceSync>(&id, headers, &body).await?;\n        tokio::spawn(async move {\n          let span = info_span!(\"ResourceSyncWebhook\", id);\n          async {\n            let res = handle_sync_webhook::<P>(\n              option, sync, body,\n            )\n            .await;\n            if let Err(e) = res {\n              warn!(\n                \"Failed at running webhook for resource sync {id} | {e:#}\"\n              );\n            }\n          }\n          .instrument(span)\n          .await\n        });\n        serror::Result::Ok(())\n      },\n    ),\n  )\n  .route(\n    \"/procedure/{id}/{branch}\",\n    post(\n      |Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {\n        let procedure =\n          auth_webhook::<P, Procedure>(&id, headers, &body).await?;\n        tokio::spawn(async move {\n          let span = info_span!(\"ProcedureWebhook\", id);\n          async {\n            let res = handle_procedure_webhook::<P>(\n              procedure, &branch, body,\n            )\n            .await;\n            if let Err(e) = res {\n              warn!(\n                \"Failed at running webhook for procedure {id} | target branch: {branch} | {e:#}\"\n              );\n            }\n          }\n          .instrument(span)\n          .await\n        });\n        serror::Result::Ok(())\n      },\n    ),\n  )\n  .route(\n    \"/action/{id}/{branch}\",\n    post(\n      |Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {\n        let action =\n          auth_webhook::<P, Action>(&id, headers, &body).await?;\n        tokio::spawn(async move {\n          let span = info_span!(\"ActionWebhook\", id);\n          async {\n            let res = handle_action_webhook::<P>(\n              action, &branch, body,\n            )\n            .await;\n            if let Err(e) = res {\n              warn!(\n                \"Failed at running webhook for action {id} | target branch: {branch} | {e:#}\"\n              );\n            }\n          }\n          .instrument(span)\n          .await\n        });\n        serror::Result::Ok(())\n      },\n    ),\n  )\n}\n\nasync fn auth_webhook<P, R>(\n  id: &str,\n  headers: HeaderMap,\n  body: &str,\n) -> serror::Result<Resource<R::Config, R::Info>>\nwhere\n  P: VerifySecret,\n  R: KomodoResource + CustomSecret,\n{\n  let resource = crate::resource::get::<R>(id)\n    .await\n    .status_code(StatusCode::BAD_REQUEST)?;\n  P::verify_secret(headers, body, R::custom_secret(&resource))\n    .status_code(StatusCode::UNAUTHORIZED)?;\n  Ok(resource)\n}\n"
  },
  {
    "path": "bin/core/src/main.rs",
    "content": "#[macro_use]\nextern crate tracing;\n\nuse std::{net::SocketAddr, str::FromStr};\n\nuse anyhow::Context;\nuse axum::Router;\nuse axum_server::{Handle, tls_rustls::RustlsConfig};\nuse tower_http::{\n  cors::{Any, CorsLayer},\n  services::{ServeDir, ServeFile},\n};\n\nuse crate::config::core_config;\n\nmod alert;\nmod api;\nmod auth;\nmod cloud;\nmod config;\nmod helpers;\nmod listener;\nmod monitor;\nmod network;\nmod permission;\nmod resource;\nmod schedule;\nmod stack;\nmod startup;\nmod state;\nmod sync;\nmod ts_client;\nmod ws;\n\nasync fn app() -> anyhow::Result<()> {\n  dotenvy::dotenv().ok();\n  let config = core_config();\n  logger::init(&config.logging)?;\n  if let Err(e) =\n    rustls::crypto::aws_lc_rs::default_provider().install_default()\n  {\n    error!(\"Failed to install default crypto provider | {e:?}\");\n    std::process::exit(1);\n  };\n\n  info!(\"Komodo Core version: v{}\", env!(\"CARGO_PKG_VERSION\"));\n\n  match (\n    config.pretty_startup_config,\n    config.unsafe_unsanitized_startup_config,\n  ) {\n    (true, true) => info!(\"{:#?}\", config),\n    (true, false) => info!(\"{:#?}\", config.sanitized()),\n    (false, true) => info!(\"{:?}\", config),\n    (false, false) => info!(\"{:?}\", config.sanitized()),\n  }\n\n  // Init jwt client to crash on failure\n  state::jwt_client();\n  tokio::join!(\n    // Init db_client check to crash on db init failure\n    state::init_db_client(),\n    // Manage OIDC client (defined in config / env vars / compose secret file)\n    auth::oidc::client::spawn_oidc_client_management()\n  );\n  // Run after db connection.\n  startup::on_startup().await;\n\n  // Spawn background tasks\n  monitor::spawn_monitor_loop();\n  resource::spawn_resource_refresh_loop();\n  resource::spawn_all_resources_cache_refresh_loop();\n  resource::spawn_build_state_refresh_loop();\n  resource::spawn_repo_state_refresh_loop();\n  resource::spawn_procedure_state_refresh_loop();\n  resource::spawn_action_state_refresh_loop();\n  schedule::spawn_schedule_executor();\n  helpers::prune::spawn_prune_loop();\n\n  // Setup static frontend services\n  let frontend_path = &config.frontend_path;\n  let frontend_index =\n    ServeFile::new(format!(\"{frontend_path}/index.html\"));\n  let serve_frontend = ServeDir::new(frontend_path)\n    .not_found_service(frontend_index.clone());\n\n  let app = Router::new()\n    .nest(\"/auth\", api::auth::router())\n    .nest(\"/user\", api::user::router())\n    .nest(\"/read\", api::read::router())\n    .nest(\"/write\", api::write::router())\n    .nest(\"/execute\", api::execute::router())\n    .nest(\"/terminal\", api::terminal::router())\n    .nest(\"/listener\", listener::router())\n    .nest(\"/ws\", ws::router())\n    .nest(\"/client\", ts_client::router())\n    .fallback_service(serve_frontend)\n    .layer(\n      CorsLayer::new()\n        .allow_origin(Any)\n        .allow_methods(Any)\n        .allow_headers(Any),\n    )\n    .into_make_service();\n\n  let addr =\n    format!(\"{}:{}\", core_config().bind_ip, core_config().port);\n  let socket_addr = SocketAddr::from_str(&addr)\n    .context(\"failed to parse listen address\")?;\n\n  let handle = Handle::new();\n  tokio::spawn({\n    // Cannot run actions until the server is available.\n    // We can use a handle for the server, and wait until\n    // the handle is listening before running actions\n    let handle = handle.clone();\n    async move {\n      handle.listening().await;\n      startup::run_startup_actions().await;\n    }\n  });\n\n  if config.ssl_enabled {\n    info!(\"🔒 Core SSL Enabled\");\n    rustls::crypto::ring::default_provider()\n      .install_default()\n      .expect(\"failed to install default rustls CryptoProvider\");\n    info!(\"Komodo Core starting on https://{socket_addr}\");\n    let ssl_config = RustlsConfig::from_pem_file(\n      &config.ssl_cert_file,\n      &config.ssl_key_file,\n    )\n    .await\n    .context(\"Invalid ssl cert / key\")?;\n    axum_server::bind_rustls(socket_addr, ssl_config)\n      .handle(handle)\n      .serve(app)\n      .await\n      .context(\"failed to start https server\")\n  } else {\n    info!(\"🔓 Core SSL Disabled\");\n    info!(\"Komodo Core starting on http://{socket_addr}\");\n    axum_server::bind(socket_addr)\n      .handle(handle)\n      .serve(app)\n      .await\n      .context(\"failed to start http server\")\n  }\n}\n\n#[tokio::main]\nasync fn main() -> anyhow::Result<()> {\n  let mut term_signal = tokio::signal::unix::signal(\n    tokio::signal::unix::SignalKind::terminate(),\n  )?;\n  tokio::select! {\n    res = tokio::spawn(app()) => res?,\n    _ = term_signal.recv() => Ok(()),\n  }\n}\n"
  },
  {
    "path": "bin/core/src/monitor/alert/deployment.rs",
    "content": "use std::collections::HashMap;\n\nuse komodo_client::entities::{\n  ResourceTarget,\n  alert::{Alert, AlertData, SeverityLevel},\n  deployment::{Deployment, DeploymentState},\n};\n\nuse crate::{\n  alert::send_alerts,\n  monitor::deployment_status_cache,\n  resource,\n  state::{action_states, db_client},\n};\n\n#[instrument(level = \"debug\")]\npub async fn alert_deployments(\n  ts: i64,\n  server_names: &HashMap<String, String>,\n) {\n  let mut alerts = Vec::<Alert>::new();\n  let action_states = action_states();\n  for status in deployment_status_cache().get_list().await {\n    // Don't alert if prev None\n    let Some(prev) = status.prev else {\n      continue;\n    };\n\n    // Don't alert if either prev or curr is Unknown.\n    // This will happen if server is unreachable, so this would be redundant.\n    if status.curr.state == DeploymentState::Unknown\n      || prev == DeploymentState::Unknown\n    {\n      continue;\n    }\n\n    // Don't alert if deploying\n    if action_states\n      .deployment\n      .get(&status.curr.id)\n      .await\n      .map(|s| s.get().map(|s| s.deploying))\n      .transpose()\n      .ok()\n      .flatten()\n      .unwrap_or_default()\n    {\n      continue;\n    }\n\n    if status.curr.state != prev {\n      // send alert\n      let Ok(deployment) =\n        resource::get::<Deployment>(&status.curr.id)\n          .await\n          .inspect_err(|e| {\n            error!(\"failed to get deployment from db | {e:#?}\")\n          })\n      else {\n        continue;\n      };\n      if !deployment.config.send_alerts {\n        continue;\n      }\n      let target: ResourceTarget = (&deployment).into();\n      let data = AlertData::ContainerStateChange {\n        id: status.curr.id.clone(),\n        name: deployment.name,\n        server_name: server_names\n          .get(&deployment.config.server_id)\n          .cloned()\n          .unwrap_or(String::from(\"unknown\")),\n        server_id: deployment.config.server_id,\n        from: prev,\n        to: status.curr.state,\n      };\n      let alert = Alert {\n        id: Default::default(),\n        level: SeverityLevel::Warning,\n        resolved: true,\n        resolved_ts: ts.into(),\n        target,\n        data,\n        ts,\n      };\n      alerts.push(alert);\n    }\n  }\n  if alerts.is_empty() {\n    return;\n  }\n  send_alerts(&alerts).await;\n  let res = db_client().alerts.insert_many(alerts).await;\n  if let Err(e) = res {\n    error!(\"failed to record deployment status alerts to db | {e:#}\");\n  }\n}\n"
  },
  {
    "path": "bin/core/src/monitor/alert/mod.rs",
    "content": "use std::collections::HashMap;\n\nuse anyhow::Context;\nuse komodo_client::entities::{\n  permission::PermissionLevel, resource::ResourceQuery,\n  server::Server, user::User,\n};\n\nuse crate::resource;\n\nmod deployment;\nmod server;\nmod stack;\n\n// called after cache update\n#[instrument(level = \"debug\")]\npub async fn check_alerts(ts: i64) {\n  let (servers, server_names) = match get_all_servers_map().await {\n    Ok(res) => res,\n    Err(e) => {\n      error!(\"{e:#?}\");\n      return;\n    }\n  };\n\n  tokio::join!(\n    server::alert_servers(ts, servers),\n    deployment::alert_deployments(ts, &server_names),\n    stack::alert_stacks(ts, &server_names)\n  );\n}\n\n#[instrument(level = \"debug\")]\nasync fn get_all_servers_map()\n-> anyhow::Result<(HashMap<String, Server>, HashMap<String, String>)>\n{\n  let servers = resource::list_full_for_user::<Server>(\n    ResourceQuery::default(),\n    &User {\n      admin: true,\n      ..Default::default()\n    },\n    PermissionLevel::Read.into(),\n    &[],\n  )\n  .await\n  .context(\"failed to get servers from db (in alert_servers)\")?;\n\n  let servers = servers\n    .into_iter()\n    .map(|server| (server.id.clone(), server))\n    .collect::<HashMap<_, _>>();\n\n  let server_names = servers\n    .iter()\n    .map(|(id, server)| (id.clone(), server.name.clone()))\n    .collect::<HashMap<_, _>>();\n\n  Ok((servers, server_names))\n}\n"
  },
  {
    "path": "bin/core/src/monitor/alert/server.rs",
    "content": "use std::{\n  collections::HashMap,\n  path::PathBuf,\n  str::FromStr,\n  sync::{Mutex, OnceLock},\n};\n\nuse anyhow::Context;\nuse database::mongo_indexed::Indexed;\nuse database::mungos::{\n  bulk_update::{self, BulkUpdate},\n  find::find_collect,\n  mongodb::bson::{doc, oid::ObjectId, to_bson},\n};\nuse derive_variants::ExtractVariant;\nuse komodo_client::entities::{\n  ResourceTarget,\n  alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},\n  komodo_timestamp, optional_string,\n  server::{Server, ServerState},\n};\n\nuse crate::{\n  alert::send_alerts,\n  helpers::maintenance::is_in_maintenance,\n  state::{db_client, server_status_cache},\n};\n\ntype SendAlerts = bool;\ntype OpenAlertMap<T = AlertDataVariant> =\n  HashMap<ResourceTarget, HashMap<T, Alert>>;\ntype OpenDiskAlertMap = OpenAlertMap<PathBuf>;\n\n/// Alert buffer to prevent immediate alerts on transient issues\nstruct AlertBuffer {\n  buffer: Mutex<HashMap<(String, AlertDataVariant), bool>>,\n}\n\nimpl AlertBuffer {\n  fn new() -> Self {\n    Self {\n      buffer: Mutex::new(HashMap::new()),\n    }\n  }\n\n  /// Check if alert should be opened. Requires two consecutive calls to return true.\n  fn ready_to_open(\n    &self,\n    server_id: String,\n    variant: AlertDataVariant,\n  ) -> bool {\n    let mut lock = self.buffer.lock().unwrap();\n    let ready = lock.entry((server_id, variant)).or_default();\n    if *ready {\n      *ready = false;\n      true\n    } else {\n      *ready = true;\n      false\n    }\n  }\n\n  /// Reset buffer state for a specific server/alert combination\n  fn reset(&self, server_id: String, variant: AlertDataVariant) {\n    let mut lock = self.buffer.lock().unwrap();\n    lock.remove(&(server_id, variant));\n  }\n}\n\n/// Global alert buffer instance\nfn alert_buffer() -> &'static AlertBuffer {\n  static BUFFER: OnceLock<AlertBuffer> = OnceLock::new();\n  BUFFER.get_or_init(AlertBuffer::new)\n}\n\n#[instrument(level = \"debug\")]\npub async fn alert_servers(\n  ts: i64,\n  mut servers: HashMap<String, Server>,\n) {\n  let server_statuses = server_status_cache().get_list().await;\n\n  let (open_alerts, open_disk_alerts) = match get_open_alerts().await\n  {\n    Ok(alerts) => alerts,\n    Err(e) => {\n      error!(\"{e:#}\");\n      return;\n    }\n  };\n\n  let mut alerts_to_open = Vec::<(Alert, SendAlerts)>::new();\n  let mut alerts_to_update = Vec::<(Alert, SendAlerts)>::new();\n  let mut alert_ids_to_close = Vec::<(Alert, SendAlerts)>::new();\n\n  let buffer = alert_buffer();\n\n  for server_status in server_statuses {\n    let Some(server) = servers.remove(&server_status.id) else {\n      continue;\n    };\n    let server_alerts = open_alerts\n      .get(&ResourceTarget::Server(server_status.id.clone()));\n\n    // Check if server is in maintenance mode\n    let in_maintenance =\n      is_in_maintenance(&server.config.maintenance_windows, ts);\n\n    // ===================\n    // SERVER HEALTH\n    // ===================\n    let health_alert = server_alerts.as_ref().and_then(|alerts| {\n      alerts.get(&AlertDataVariant::ServerUnreachable)\n    });\n    match (server_status.state, health_alert) {\n      (ServerState::NotOk, None) => {\n        // Only open unreachable alert if not in maintenance and buffer is ready\n        if !in_maintenance\n          && buffer.ready_to_open(\n            server_status.id.clone(),\n            AlertDataVariant::ServerUnreachable,\n          )\n        {\n          let alert = Alert {\n            id: Default::default(),\n            ts,\n            resolved: false,\n            resolved_ts: None,\n            level: SeverityLevel::Critical,\n            target: ResourceTarget::Server(server_status.id.clone()),\n            data: AlertData::ServerUnreachable {\n              id: server_status.id.clone(),\n              name: server.name.clone(),\n              region: optional_string(&server.config.region),\n              err: server_status.err.clone(),\n            },\n          };\n          alerts_to_open\n            .push((alert, server.config.send_unreachable_alerts))\n        }\n      }\n      (ServerState::NotOk, Some(alert)) => {\n        // update alert err\n        let mut alert = alert.clone();\n        let (id, name, region) = match alert.data {\n          AlertData::ServerUnreachable {\n            id, name, region, ..\n          } => (id, name, region),\n          data => {\n            error!(\n              \"got incorrect alert data in ServerStatus handler. got {data:?}\"\n            );\n            continue;\n          }\n        };\n        alert.data = AlertData::ServerUnreachable {\n          id,\n          name,\n          region,\n          err: server_status.err.clone(),\n        };\n\n        // Never send this alert, severity is always 'Critical'\n        alerts_to_update.push((alert, false));\n      }\n\n      // Close an open alert\n      (ServerState::Ok | ServerState::Disabled, Some(alert)) => {\n        alert_ids_to_close.push((\n          alert.clone(),\n          server.config.send_unreachable_alerts,\n        ));\n      }\n      (ServerState::Ok | ServerState::Disabled, None) => buffer\n        .reset(\n          server_status.id.clone(),\n          AlertDataVariant::ServerUnreachable,\n        ),\n    }\n\n    // ===================\n    // SERVER VERSION MISMATCH\n    // ===================\n    let core_version = env!(\"CARGO_PKG_VERSION\");\n    let has_version_mismatch = server_status.state == ServerState::Ok\n      && !server_status.version.is_empty()\n      && server_status.version != \"Unknown\"\n      && server_status.version != core_version;\n\n    let version_alert = server_alerts.as_ref().and_then(|alerts| {\n      alerts.get(&AlertDataVariant::ServerVersionMismatch)\n    });\n\n    match (has_version_mismatch, version_alert) {\n      (true, None) => {\n        // Only open version mismatch alert if not in maintenance and buffer is ready\n        if !in_maintenance\n          && buffer.ready_to_open(\n            server_status.id.clone(),\n            AlertDataVariant::ServerVersionMismatch,\n          )\n        {\n          let alert = Alert {\n            id: Default::default(),\n            ts,\n            resolved: false,\n            resolved_ts: None,\n            level: SeverityLevel::Warning,\n            target: ResourceTarget::Server(server_status.id.clone()),\n            data: AlertData::ServerVersionMismatch {\n              id: server_status.id.clone(),\n              name: server.name.clone(),\n              region: optional_string(&server.config.region),\n              server_version: server_status.version.clone(),\n              core_version: core_version.to_string(),\n            },\n          };\n          // Use send_unreachable_alerts as a proxy for general server alerts\n          alerts_to_open\n            .push((alert, server.config.send_version_mismatch_alerts))\n        }\n      }\n      (true, Some(alert)) => {\n        // Update existing alert with current version info\n        let mut alert = alert.clone();\n        alert.data = AlertData::ServerVersionMismatch {\n          id: server_status.id.clone(),\n          name: server.name.clone(),\n          region: optional_string(&server.config.region),\n          server_version: server_status.version.clone(),\n          core_version: core_version.to_string(),\n        };\n        // Don't send notification for updates\n        alerts_to_update.push((alert, false));\n      }\n      (false, Some(alert)) => {\n        // Version is now correct, close the alert\n        alert_ids_to_close.push((\n          alert.clone(),\n          server.config.send_version_mismatch_alerts,\n        ));\n      }\n      (false, None) => {\n        // Reset buffer state when no mismatch and no alert\n        buffer.reset(\n          server_status.id.clone(),\n          AlertDataVariant::ServerVersionMismatch,\n        )\n      }\n    }\n\n    let Some(health) = &server_status.health else {\n      continue;\n    };\n\n    // ===================\n    // SERVER CPU\n    // ===================\n    let cpu_alert = server_alerts\n      .as_ref()\n      .and_then(|alerts| alerts.get(&AlertDataVariant::ServerCpu))\n      .cloned();\n    match (health.cpu.level, cpu_alert, health.cpu.should_close_alert)\n    {\n      (SeverityLevel::Warning | SeverityLevel::Critical, None, _) => {\n        // Only open CPU alert if not in maintenance and buffer is ready\n        if !in_maintenance\n          && buffer.ready_to_open(\n            server_status.id.clone(),\n            AlertDataVariant::ServerCpu,\n          )\n        {\n          let alert = Alert {\n            id: Default::default(),\n            ts,\n            resolved: false,\n            resolved_ts: None,\n            level: health.cpu.level,\n            target: ResourceTarget::Server(server_status.id.clone()),\n            data: AlertData::ServerCpu {\n              id: server_status.id.clone(),\n              name: server.name.clone(),\n              region: optional_string(&server.config.region),\n              percentage: server_status\n                .stats\n                .as_ref()\n                .map(|s| s.cpu_perc as f64)\n                .unwrap_or(0.0),\n            },\n          };\n          alerts_to_open.push((alert, server.config.send_cpu_alerts));\n        }\n      }\n      (\n        SeverityLevel::Warning | SeverityLevel::Critical,\n        Some(mut alert),\n        _,\n      ) => {\n        // modify alert level only if it has increased and not in maintenance\n        if !in_maintenance && alert.level < health.cpu.level {\n          alert.level = health.cpu.level;\n          alert.data = AlertData::ServerCpu {\n            id: server_status.id.clone(),\n            name: server.name.clone(),\n            region: optional_string(&server.config.region),\n            percentage: server_status\n              .stats\n              .as_ref()\n              .map(|s| s.cpu_perc as f64)\n              .unwrap_or(0.0),\n          };\n          alerts_to_update\n            .push((alert, server.config.send_cpu_alerts));\n        }\n      }\n      (SeverityLevel::Ok, Some(alert), true) => {\n        let mut alert = alert.clone();\n        alert.data = AlertData::ServerCpu {\n          id: server_status.id.clone(),\n          name: server.name.clone(),\n          region: optional_string(&server.config.region),\n          percentage: server_status\n            .stats\n            .as_ref()\n            .map(|s| s.cpu_perc as f64)\n            .unwrap_or(0.0),\n        };\n        alert_ids_to_close\n          .push((alert, server.config.send_cpu_alerts))\n      }\n      (SeverityLevel::Ok, _, _) => buffer\n        .reset(server_status.id.clone(), AlertDataVariant::ServerCpu),\n    }\n\n    // ===================\n    // SERVER MEM\n    // ===================\n    let mem_alert = server_alerts\n      .as_ref()\n      .and_then(|alerts| alerts.get(&AlertDataVariant::ServerMem))\n      .cloned();\n    match (health.mem.level, mem_alert, health.mem.should_close_alert)\n    {\n      (SeverityLevel::Warning | SeverityLevel::Critical, None, _) => {\n        // Only open memory alert if not in maintenance and buffer is ready\n        if !in_maintenance\n          && buffer.ready_to_open(\n            server_status.id.clone(),\n            AlertDataVariant::ServerMem,\n          )\n        {\n          let alert = Alert {\n            id: Default::default(),\n            ts,\n            resolved: false,\n            resolved_ts: None,\n            level: health.mem.level,\n            target: ResourceTarget::Server(server_status.id.clone()),\n            data: AlertData::ServerMem {\n              id: server_status.id.clone(),\n              name: server.name.clone(),\n              region: optional_string(&server.config.region),\n              total_gb: server_status\n                .stats\n                .as_ref()\n                .map(|s| s.mem_total_gb)\n                .unwrap_or(0.0),\n              used_gb: server_status\n                .stats\n                .as_ref()\n                .map(|s| s.mem_used_gb)\n                .unwrap_or(0.0),\n            },\n          };\n          alerts_to_open.push((alert, server.config.send_mem_alerts));\n        }\n      }\n      (\n        SeverityLevel::Warning | SeverityLevel::Critical,\n        Some(mut alert),\n        _,\n      ) => {\n        // modify alert level only if it has increased and not in maintenance\n        if !in_maintenance && alert.level < health.mem.level {\n          alert.level = health.mem.level;\n          alert.data = AlertData::ServerMem {\n            id: server_status.id.clone(),\n            name: server.name.clone(),\n            region: optional_string(&server.config.region),\n            total_gb: server_status\n              .stats\n              .as_ref()\n              .map(|s| s.mem_total_gb)\n              .unwrap_or(0.0),\n            used_gb: server_status\n              .stats\n              .as_ref()\n              .map(|s| s.mem_used_gb)\n              .unwrap_or(0.0),\n          };\n          alerts_to_update\n            .push((alert, server.config.send_mem_alerts));\n        }\n      }\n      (SeverityLevel::Ok, Some(alert), true) => {\n        let mut alert = alert.clone();\n        alert.data = AlertData::ServerMem {\n          id: server_status.id.clone(),\n          name: server.name.clone(),\n          region: optional_string(&server.config.region),\n          total_gb: server_status\n            .stats\n            .as_ref()\n            .map(|s| s.mem_total_gb)\n            .unwrap_or(0.0),\n          used_gb: server_status\n            .stats\n            .as_ref()\n            .map(|s| s.mem_used_gb)\n            .unwrap_or(0.0),\n        };\n        alert_ids_to_close\n          .push((alert, server.config.send_mem_alerts))\n      }\n      (SeverityLevel::Ok, _, _) => buffer\n        .reset(server_status.id.clone(), AlertDataVariant::ServerMem),\n    }\n\n    // ===================\n    // SERVER DISK\n    // ===================\n\n    let server_disk_alerts = open_disk_alerts\n      .get(&ResourceTarget::Server(server_status.id.clone()));\n\n    for (path, health) in &health.disks {\n      let disk_alert = server_disk_alerts\n        .as_ref()\n        .and_then(|alerts| alerts.get(path))\n        .cloned();\n      match (health.level, disk_alert, health.should_close_alert) {\n        (\n          SeverityLevel::Warning | SeverityLevel::Critical,\n          None,\n          _,\n        ) => {\n          // Only open disk alert if not in maintenance and buffer is ready\n          if !in_maintenance\n            && buffer.ready_to_open(\n              server_status.id.clone(),\n              AlertDataVariant::ServerDisk,\n            )\n          {\n            let disk =\n              server_status.stats.as_ref().and_then(|stats| {\n                stats.disks.iter().find(|disk| disk.mount == *path)\n              });\n            let alert = Alert {\n              id: Default::default(),\n              ts,\n              resolved: false,\n              resolved_ts: None,\n              level: health.level,\n              target: ResourceTarget::Server(\n                server_status.id.clone(),\n              ),\n              data: AlertData::ServerDisk {\n                id: server_status.id.clone(),\n                name: server.name.clone(),\n                region: optional_string(&server.config.region),\n                path: path.to_owned(),\n                total_gb: disk\n                  .map(|d| d.total_gb)\n                  .unwrap_or_default(),\n                used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),\n              },\n            };\n            alerts_to_open\n              .push((alert, server.config.send_disk_alerts));\n          }\n        }\n        (\n          SeverityLevel::Warning | SeverityLevel::Critical,\n          Some(mut alert),\n          _,\n        ) => {\n          // modify alert level only if it has increased and not in maintenance\n          if !in_maintenance && health.level < alert.level {\n            let disk =\n              server_status.stats.as_ref().and_then(|stats| {\n                stats.disks.iter().find(|disk| disk.mount == *path)\n              });\n            alert.level = health.level;\n            alert.data = AlertData::ServerDisk {\n              id: server_status.id.clone(),\n              name: server.name.clone(),\n              region: optional_string(&server.config.region),\n              path: path.to_owned(),\n              total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),\n              used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),\n            };\n            alerts_to_update\n              .push((alert, server.config.send_disk_alerts));\n          }\n        }\n        (SeverityLevel::Ok, Some(alert), true) => {\n          let mut alert = alert.clone();\n          let disk = server_status.stats.as_ref().and_then(|stats| {\n            stats.disks.iter().find(|disk| disk.mount == *path)\n          });\n          alert.level = health.level;\n          alert.data = AlertData::ServerDisk {\n            id: server_status.id.clone(),\n            name: server.name.clone(),\n            region: optional_string(&server.config.region),\n            path: path.to_owned(),\n            total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),\n            used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),\n          };\n          alert_ids_to_close\n            .push((alert, server.config.send_disk_alerts))\n        }\n        (SeverityLevel::Ok, _, _) => buffer.reset(\n          server_status.id.clone(),\n          AlertDataVariant::ServerDisk,\n        ),\n      }\n    }\n\n    // Need to close any open ones on disks no longer reported\n    if let Some(disk_alerts) = server_disk_alerts {\n      for (path, alert) in disk_alerts {\n        if !health.disks.contains_key(path) {\n          let mut alert = alert.clone();\n          alert.level = SeverityLevel::Ok;\n          alert_ids_to_close\n            .push((alert, server.config.send_disk_alerts));\n        }\n      }\n    }\n  }\n\n  tokio::join!(\n    open_new_alerts(&alerts_to_open),\n    update_alerts(&alerts_to_update),\n    resolve_alerts(&alert_ids_to_close),\n  );\n}\n\n#[instrument(level = \"debug\")]\nasync fn open_new_alerts(alerts: &[(Alert, SendAlerts)]) {\n  if alerts.is_empty() {\n    return;\n  }\n\n  let db = db_client();\n\n  let open = || async {\n    let ids = db\n      .alerts\n      .insert_many(alerts.iter().map(|(alert, _)| alert))\n      .await?\n      .inserted_ids\n      .into_iter()\n      .filter_map(|(index, id)| {\n        alerts.get(index)?.1.then(|| id.as_object_id())\n      })\n      .flatten()\n      .collect::<Vec<_>>();\n    anyhow::Ok(ids)\n  };\n\n  let ids_to_send = match open().await {\n    Ok(ids) => ids,\n    Err(e) => {\n      error!(\"failed to open alerts on db | {e:?}\");\n      return;\n    }\n  };\n\n  let alerts = match find_collect(\n    &db.alerts,\n    doc! { \"_id\": { \"$in\": ids_to_send } },\n    None,\n  )\n  .await\n  {\n    Ok(alerts) => alerts,\n    Err(e) => {\n      error!(\"failed to pull created alerts from mongo | {e:?}\");\n      return;\n    }\n  };\n\n  send_alerts(&alerts).await\n}\n\n#[instrument(level = \"debug\")]\nasync fn update_alerts(alerts: &[(Alert, SendAlerts)]) {\n  if alerts.is_empty() {\n    return;\n  }\n\n  let open = || async {\n    let updates = alerts.iter().map(|(alert, _)| {\n        let update = BulkUpdate {\n          query: doc! { \"_id\": ObjectId::from_str(&alert.id).context(\"failed to convert alert id to ObjectId\")? },\n          update: doc! { \"$set\": to_bson(alert).context(\"failed to convert alert to bson\")? }\n        };\n        anyhow::Ok(update)\n      })\n      .filter_map(|update| match update {\n        Ok(update) => Some(update),\n        Err(e) => {\n          warn!(\"failed to generate bulk update for alert | {e:#}\");\n          None\n        }\n      }).collect::<Vec<_>>();\n\n    bulk_update::bulk_update(\n      &db_client().db,\n      Alert::default_collection_name(),\n      &updates,\n      false,\n    )\n    .await\n    .context(\"failed to bulk update alerts\")?;\n\n    anyhow::Ok(())\n  };\n\n  let alerts = alerts\n    .iter()\n    .filter(|(_, send)| *send)\n    .map(|(alert, _)| alert)\n    .cloned()\n    .collect::<Vec<_>>();\n\n  let (res, _) = tokio::join!(open(), send_alerts(&alerts));\n\n  if let Err(e) = res {\n    error!(\"failed to create alerts on db | {e:#}\");\n  }\n}\n\n#[instrument(level = \"debug\")]\nasync fn resolve_alerts(alerts: &[(Alert, SendAlerts)]) {\n  if alerts.is_empty() {\n    return;\n  }\n\n  let close = || async move {\n    let alert_ids = alerts\n      .iter()\n      .map(|(alert, _)| {\n        ObjectId::from_str(&alert.id)\n          .context(\"failed to convert alert id to ObjectId\")\n      })\n      .collect::<anyhow::Result<Vec<_>>>()?;\n\n    db_client()\n      .alerts\n      .update_many(\n        doc! { \"_id\": { \"$in\": &alert_ids } },\n        doc! {\n          \"$set\": {\n            \"resolved\": true,\n            \"resolved_ts\": komodo_timestamp()\n          }\n        },\n      )\n      .await\n      .context(\"failed to resolve alerts on db\")\n      .inspect_err(|e| warn!(\"{e:#}\"))\n      .ok();\n\n    let ts = komodo_timestamp();\n\n    let closed = alerts\n      .iter()\n      .filter(|(_, send)| *send)\n      .map(|(alert, _)| {\n        let mut alert = alert.clone();\n\n        alert.resolved = true;\n        alert.resolved_ts = Some(ts);\n        alert.level = SeverityLevel::Ok;\n\n        alert\n      })\n      .collect::<Vec<_>>();\n\n    send_alerts(&closed).await;\n\n    anyhow::Ok(())\n  };\n\n  if let Err(e) = close().await {\n    error!(\"failed to resolve alerts | {e:#?}\");\n  }\n}\n\n#[instrument(level = \"debug\")]\nasync fn get_open_alerts()\n-> anyhow::Result<(OpenAlertMap, OpenDiskAlertMap)> {\n  let alerts = find_collect(\n    &db_client().alerts,\n    doc! { \"resolved\": false },\n    None,\n  )\n  .await\n  .context(\"failed to get open alerts from db\")?;\n\n  let mut map = OpenAlertMap::new();\n  let mut disk_map = OpenDiskAlertMap::new();\n\n  for alert in alerts {\n    match &alert.data {\n      AlertData::ServerDisk { path, .. } => {\n        let inner = disk_map.entry(alert.target.clone()).or_default();\n        inner.insert(path.to_owned(), alert);\n      }\n      _ => {\n        let inner = map.entry(alert.target.clone()).or_default();\n        inner.insert(alert.data.extract_variant(), alert);\n      }\n    }\n  }\n\n  Ok((map, disk_map))\n}\n"
  },
  {
    "path": "bin/core/src/monitor/alert/stack.rs",
    "content": "use std::collections::HashMap;\n\nuse komodo_client::entities::{\n  ResourceTarget,\n  alert::{Alert, AlertData, SeverityLevel},\n  stack::{Stack, StackState},\n};\n\nuse crate::{\n  alert::send_alerts,\n  resource,\n  state::{action_states, db_client, stack_status_cache},\n};\n\n#[instrument(level = \"debug\")]\npub async fn alert_stacks(\n  ts: i64,\n  server_names: &HashMap<String, String>,\n) {\n  let action_states = action_states();\n  let mut alerts = Vec::<Alert>::new();\n  for status in stack_status_cache().get_list().await {\n    // Don't alert if prev None\n    let Some(prev) = status.prev else {\n      continue;\n    };\n\n    // Don't alert if either prev or curr is Unknown.\n    // This will happen if server is unreachable, so this would be redundant.\n    if status.curr.state == StackState::Unknown\n      || prev == StackState::Unknown\n    {\n      continue;\n    }\n\n    // Don't alert if deploying\n    if action_states\n      .stack\n      .get(&status.curr.id)\n      .await\n      .map(|s| s.get().map(|s| s.deploying))\n      .transpose()\n      .ok()\n      .flatten()\n      .unwrap_or_default()\n    {\n      continue;\n    }\n\n    if status.curr.state != prev {\n      // send alert\n      let Ok(stack) =\n        resource::get::<Stack>(&status.curr.id).await.inspect_err(\n          |e| error!(\"failed to get stack from db | {e:#?}\"),\n        )\n      else {\n        continue;\n      };\n      if !stack.config.send_alerts {\n        continue;\n      }\n      let target: ResourceTarget = (&stack).into();\n      let data = AlertData::StackStateChange {\n        id: status.curr.id.clone(),\n        name: stack.name,\n        server_name: server_names\n          .get(&stack.config.server_id)\n          .cloned()\n          .unwrap_or(String::from(\"unknown\")),\n        server_id: stack.config.server_id,\n        from: prev,\n        to: status.curr.state,\n      };\n      let alert = Alert {\n        id: Default::default(),\n        level: SeverityLevel::Warning,\n        resolved: true,\n        resolved_ts: ts.into(),\n        target,\n        data,\n        ts,\n      };\n      alerts.push(alert);\n    }\n  }\n  if alerts.is_empty() {\n    return;\n  }\n  send_alerts(&alerts).await;\n  let res = db_client().alerts.insert_many(alerts).await;\n  if let Err(e) = res {\n    error!(\"failed to record stack status alerts to db | {e:#}\");\n  }\n}\n"
  },
  {
    "path": "bin/core/src/monitor/helpers.rs",
    "content": "use komodo_client::entities::{\n  alert::SeverityLevel,\n  deployment::{Deployment, DeploymentState},\n  docker::{\n    container::ContainerListItem, image::ImageListItem,\n    network::NetworkListItem, volume::VolumeListItem,\n  },\n  repo::Repo,\n  server::{\n    Server, ServerConfig, ServerHealth, ServerHealthState,\n    ServerState,\n  },\n  stack::{ComposeProject, Stack, StackState},\n  stats::{SingleDiskUsage, SystemStats},\n};\nuse serror::Serror;\n\nuse crate::state::{\n  deployment_status_cache, repo_status_cache, server_status_cache,\n  stack_status_cache,\n};\n\nuse super::{\n  CachedDeploymentStatus, CachedRepoStatus, CachedServerStatus,\n  CachedStackStatus, History,\n};\n\n#[instrument(level = \"debug\", skip_all)]\npub async fn insert_deployments_status_unknown(\n  deployments: Vec<Deployment>,\n) {\n  let status_cache = deployment_status_cache();\n  for deployment in deployments {\n    let prev =\n      status_cache.get(&deployment.id).await.map(|s| s.curr.state);\n    status_cache\n      .insert(\n        deployment.id.clone(),\n        History {\n          curr: CachedDeploymentStatus {\n            id: deployment.id,\n            state: DeploymentState::Unknown,\n            container: None,\n            update_available: false,\n          },\n          prev,\n        }\n        .into(),\n      )\n      .await;\n  }\n}\n\n#[instrument(level = \"debug\", skip_all)]\npub async fn insert_repos_status_unknown(repos: Vec<Repo>) {\n  let status_cache = repo_status_cache();\n  for repo in repos {\n    status_cache\n      .insert(\n        repo.id.clone(),\n        CachedRepoStatus {\n          latest_hash: None,\n          latest_message: None,\n        }\n        .into(),\n      )\n      .await;\n  }\n}\n\n#[instrument(level = \"debug\", skip_all)]\npub async fn insert_stacks_status_unknown(stacks: Vec<Stack>) {\n  let status_cache = stack_status_cache();\n  for stack in stacks {\n    let prev =\n      status_cache.get(&stack.id).await.map(|s| s.curr.state);\n    status_cache\n      .insert(\n        stack.id.clone(),\n        History {\n          curr: CachedStackStatus {\n            id: stack.id,\n            state: StackState::Unknown,\n            services: Vec::new(),\n          },\n          prev,\n        }\n        .into(),\n      )\n      .await;\n  }\n}\n\ntype DockerLists = (\n  Option<Vec<ContainerListItem>>,\n  Option<Vec<NetworkListItem>>,\n  Option<Vec<ImageListItem>>,\n  Option<Vec<VolumeListItem>>,\n  Option<Vec<ComposeProject>>,\n);\n\n#[instrument(level = \"debug\", skip_all)]\npub async fn insert_server_status(\n  server: &Server,\n  state: ServerState,\n  version: String,\n  stats: Option<SystemStats>,\n  (containers, networks, images, volumes, projects): DockerLists,\n  err: impl Into<Option<Serror>>,\n) {\n  let health = stats.as_ref().map(|s| get_server_health(server, s));\n  server_status_cache()\n    .insert(\n      server.id.clone(),\n      CachedServerStatus {\n        id: server.id.clone(),\n        state,\n        version,\n        stats,\n        health,\n        containers,\n        networks,\n        images,\n        volumes,\n        projects,\n        err: err.into(),\n      }\n      .into(),\n    )\n    .await;\n}\n\nconst ALERT_PERCENTAGE_THRESHOLD: f32 = 5.0;\n\nfn get_server_health(\n  server: &Server,\n  SystemStats {\n    cpu_perc,\n    mem_used_gb,\n    mem_total_gb,\n    disks,\n    ..\n  }: &SystemStats,\n) -> ServerHealth {\n  let ServerConfig {\n    cpu_warning,\n    cpu_critical,\n    mem_warning,\n    mem_critical,\n    disk_warning,\n    disk_critical,\n    ..\n  } = &server.config;\n  let mut health = ServerHealth::default();\n\n  if cpu_perc >= cpu_critical {\n    health.cpu.level = SeverityLevel::Critical;\n  } else if cpu_perc >= cpu_warning {\n    health.cpu.level = SeverityLevel::Warning\n  } else if *cpu_perc < cpu_warning - ALERT_PERCENTAGE_THRESHOLD {\n    health.cpu.should_close_alert = true\n  }\n\n  let mem_perc = 100.0 * mem_used_gb / mem_total_gb;\n  if mem_perc >= *mem_critical {\n    health.mem.level = SeverityLevel::Critical\n  } else if mem_perc >= *mem_warning {\n    health.mem.level = SeverityLevel::Warning\n  } else if mem_perc\n    < mem_warning - (ALERT_PERCENTAGE_THRESHOLD as f64)\n  {\n    health.mem.should_close_alert = true\n  }\n\n  for SingleDiskUsage {\n    mount,\n    used_gb,\n    total_gb,\n    ..\n  } in disks\n  {\n    let perc = 100.0 * used_gb / total_gb;\n    let mut state = ServerHealthState::default();\n    if perc >= *disk_critical {\n      state.level = SeverityLevel::Critical;\n    } else if perc >= *disk_warning {\n      state.level = SeverityLevel::Warning;\n    } else if perc\n      < disk_warning - (ALERT_PERCENTAGE_THRESHOLD as f64)\n    {\n      state.should_close_alert = true;\n    };\n    health.disks.insert(mount.clone(), state);\n  }\n\n  health\n}\n"
  },
  {
    "path": "bin/core/src/monitor/lists.rs",
    "content": "use komodo_client::entities::{\n  docker::{\n    container::ContainerListItem, image::ImageListItem,\n    network::NetworkListItem, volume::VolumeListItem,\n  },\n  stack::ComposeProject,\n};\nuse periphery_client::{\n  PeripheryClient,\n  api::{GetDockerLists, GetDockerListsResponse},\n};\n\npub async fn get_docker_lists(\n  periphery: &PeripheryClient,\n) -> anyhow::Result<(\n  Vec<ContainerListItem>,\n  Vec<NetworkListItem>,\n  Vec<ImageListItem>,\n  Vec<VolumeListItem>,\n  Vec<ComposeProject>,\n)> {\n  let GetDockerListsResponse {\n    containers,\n    networks,\n    images,\n    volumes,\n    projects,\n  } = periphery.request(GetDockerLists {}).await?;\n  // TODO: handle the errors\n  let (\n    mut containers,\n    mut networks,\n    mut images,\n    mut volumes,\n    mut projects,\n  ) = (\n    containers.unwrap_or_default(),\n    networks.unwrap_or_default(),\n    images.unwrap_or_default(),\n    volumes.unwrap_or_default(),\n    projects.unwrap_or_default(),\n  );\n\n  containers.sort_by(|a, b| a.name.cmp(&b.name));\n  networks.sort_by(|a, b| a.name.cmp(&b.name));\n  images.sort_by(|a, b| a.name.cmp(&b.name));\n  volumes.sort_by(|a, b| a.name.cmp(&b.name));\n  projects.sort_by(|a, b| a.name.cmp(&b.name));\n\n  Ok((containers, networks, images, volumes, projects))\n}\n"
  },
  {
    "path": "bin/core/src/monitor/mod.rs",
    "content": "use std::sync::{Arc, OnceLock};\n\nuse async_timing_util::wait_until_timelength;\nuse database::mungos::{find::find_collect, mongodb::bson::doc};\nuse futures::future::join_all;\nuse helpers::insert_stacks_status_unknown;\nuse komodo_client::entities::{\n  deployment::DeploymentState,\n  docker::{\n    container::ContainerListItem, image::ImageListItem,\n    network::NetworkListItem, volume::VolumeListItem,\n  },\n  komodo_timestamp, optional_string,\n  server::{Server, ServerHealth, ServerState},\n  stack::{ComposeProject, StackService, StackState},\n  stats::SystemStats,\n};\nuse periphery_client::api::{self, git::GetLatestCommit};\nuse serror::Serror;\nuse tokio::sync::Mutex;\n\nuse crate::{\n  config::core_config,\n  helpers::{cache::Cache, periphery_client},\n  monitor::{alert::check_alerts, record::record_server_stats},\n  state::{db_client, deployment_status_cache, repo_status_cache},\n};\n\nuse self::helpers::{\n  insert_deployments_status_unknown, insert_repos_status_unknown,\n  insert_server_status,\n};\n\nmod alert;\nmod helpers;\nmod lists;\nmod record;\nmod resources;\n\n#[derive(Default, Debug)]\npub struct History<Curr: Default, Prev> {\n  pub curr: Curr,\n  pub prev: Option<Prev>,\n}\n\n#[derive(Default, Clone, Debug)]\npub struct CachedServerStatus {\n  pub id: String,\n  pub state: ServerState,\n  pub version: String,\n  pub stats: Option<SystemStats>,\n  pub health: Option<ServerHealth>,\n  pub containers: Option<Vec<ContainerListItem>>,\n  pub networks: Option<Vec<NetworkListItem>>,\n  pub images: Option<Vec<ImageListItem>>,\n  pub volumes: Option<Vec<VolumeListItem>>,\n  pub projects: Option<Vec<ComposeProject>>,\n  /// Store the error in reaching periphery\n  pub err: Option<serror::Serror>,\n}\n\n#[derive(Default, Clone, Debug)]\npub struct CachedDeploymentStatus {\n  /// The deployment id\n  pub id: String,\n  pub state: DeploymentState,\n  pub container: Option<ContainerListItem>,\n  pub update_available: bool,\n}\n\n#[derive(Default, Clone, Debug)]\npub struct CachedRepoStatus {\n  pub latest_hash: Option<String>,\n  pub latest_message: Option<String>,\n}\n\n#[derive(Default, Clone, Debug)]\npub struct CachedStackStatus {\n  /// The stack id\n  pub id: String,\n  /// The stack state\n  pub state: StackState,\n  /// The services connected to the stack\n  pub services: Vec<StackService>,\n}\n\nconst ADDITIONAL_MS: u128 = 500;\n\npub fn spawn_monitor_loop() {\n  let interval: async_timing_util::Timelength = core_config()\n    .monitoring_interval\n    .try_into()\n    .expect(\"Invalid monitoring interval\");\n  tokio::spawn(async move {\n    refresh_server_cache(komodo_timestamp()).await;\n    loop {\n      let ts = (wait_until_timelength(interval, ADDITIONAL_MS).await\n        - ADDITIONAL_MS) as i64;\n      refresh_server_cache(ts).await;\n    }\n  });\n}\n\nasync fn refresh_server_cache(ts: i64) {\n  let servers =\n    match find_collect(&db_client().servers, None, None).await {\n      Ok(servers) => servers,\n      Err(e) => {\n        error!(\n          \"failed to get server list (manage status cache) | {e:#}\"\n        );\n        return;\n      }\n    };\n  let futures = servers.into_iter().map(|server| async move {\n    update_cache_for_server(&server, false).await;\n  });\n  join_all(futures).await;\n  tokio::join!(check_alerts(ts), record_server_stats(ts));\n}\n\n/// Makes sure cache for server doesn't update too frequently / simultaneously.\n/// If forced, will still block against simultaneous update.\nfn update_cache_for_server_controller()\n-> &'static Cache<String, Arc<Mutex<i64>>> {\n  static CACHE: OnceLock<Cache<String, Arc<Mutex<i64>>>> =\n    OnceLock::new();\n  CACHE.get_or_init(Default::default)\n}\n\n/// The background loop will call this with force: false,\n/// which exits early if the lock is busy or it was completed too recently.\n/// If force is true, it will wait on simultaneous calls, and will\n/// ignore the restriction on being completed too recently.\n#[instrument(level = \"debug\")]\npub async fn update_cache_for_server(server: &Server, force: bool) {\n  // Concurrency controller to ensure it isn't done too often\n  // when it happens in other contexts.\n  let controller = update_cache_for_server_controller()\n    .get_or_insert_default(&server.id)\n    .await;\n  let mut lock = match controller.try_lock() {\n    Ok(lock) => lock,\n    Err(_) if force => controller.lock().await,\n    Err(_) => return,\n  };\n\n  let now = komodo_timestamp();\n\n  // early return if called again sooner than 1s.\n  if !force && *lock > now - 1_000 {\n    return;\n  }\n\n  *lock = now;\n\n  let (deployments, builds, repos, stacks) = tokio::join!(\n    find_collect(\n      &db_client().deployments,\n      doc! { \"config.server_id\": &server.id },\n      None,\n    ),\n    find_collect(&db_client().builds, doc! {}, None,),\n    find_collect(\n      &db_client().repos,\n      doc! { \"config.server_id\": &server.id },\n      None,\n    ),\n    find_collect(\n      &db_client().stacks,\n      doc! { \"config.server_id\": &server.id },\n      None,\n    )\n  );\n\n  let deployments =  deployments.inspect_err(|e| error!(\"failed to get deployments list from db (update status cache) | server : {} | {e:#}\", server.name)).unwrap_or_default();\n  let builds =  builds.inspect_err(|e| error!(\"failed to get builds list from db (update status cache) | server : {} | {e:#}\", server.name)).unwrap_or_default();\n  let repos = repos.inspect_err(|e|  error!(\"failed to get repos list from db (update status cache) | server: {} | {e:#}\", server.name)).unwrap_or_default();\n  let stacks = stacks.inspect_err(|e|  error!(\"failed to get stacks list from db (update status cache) | server: {} | {e:#}\", server.name)).unwrap_or_default();\n\n  // Handle server disabled\n  if !server.config.enabled {\n    insert_deployments_status_unknown(deployments).await;\n    insert_stacks_status_unknown(stacks).await;\n    insert_repos_status_unknown(repos).await;\n    insert_server_status(\n      server,\n      ServerState::Disabled,\n      String::from(\"unknown\"),\n      None,\n      (None, None, None, None, None),\n      None,\n    )\n    .await;\n    return;\n  }\n\n  let Ok(periphery) = periphery_client(server) else {\n    error!(\n      \"somehow periphery not ok to create. should not be reached.\"\n    );\n    return;\n  };\n\n  let version = match periphery.request(api::GetVersion {}).await {\n    Ok(version) => version.version,\n    Err(e) => {\n      insert_deployments_status_unknown(deployments).await;\n      insert_stacks_status_unknown(stacks).await;\n      insert_repos_status_unknown(repos).await;\n      insert_server_status(\n        server,\n        ServerState::NotOk,\n        String::from(\"Unknown\"),\n        None,\n        (None, None, None, None, None),\n        Serror::from(&e),\n      )\n      .await;\n      return;\n    }\n  };\n\n  let stats = if server.config.stats_monitoring {\n    match periphery.request(api::stats::GetSystemStats {}).await {\n      Ok(stats) => Some(filter_volumes(server, stats)),\n      Err(e) => {\n        insert_deployments_status_unknown(deployments).await;\n        insert_stacks_status_unknown(stacks).await;\n        insert_repos_status_unknown(repos).await;\n        insert_server_status(\n          server,\n          ServerState::NotOk,\n          String::from(\"unknown\"),\n          None,\n          (None, None, None, None, None),\n          Serror::from(&e),\n        )\n        .await;\n        return;\n      }\n    }\n  } else {\n    None\n  };\n\n  match lists::get_docker_lists(&periphery).await {\n    Ok((mut containers, networks, images, volumes, projects)) => {\n      containers.iter_mut().for_each(|container| {\n        container.server_id = Some(server.id.clone())\n      });\n      tokio::join!(\n        resources::update_deployment_cache(\n          server.name.clone(),\n          deployments,\n          &containers,\n          &images,\n          &builds,\n        ),\n        resources::update_stack_cache(\n          server.name.clone(),\n          stacks,\n          &containers,\n          &images\n        ),\n      );\n      insert_server_status(\n        server,\n        ServerState::Ok,\n        version,\n        stats,\n        (\n          Some(containers.clone()),\n          Some(networks),\n          Some(images),\n          Some(volumes),\n          Some(projects),\n        ),\n        None,\n      )\n      .await;\n    }\n    Err(e) => {\n      insert_deployments_status_unknown(deployments).await;\n      insert_stacks_status_unknown(stacks).await;\n      insert_server_status(\n        server,\n        ServerState::Ok,\n        version,\n        stats,\n        (None, None, None, None, None),\n        Some(e.into()),\n      )\n      .await;\n    }\n  }\n\n  let status_cache = repo_status_cache();\n  for repo in repos {\n    let (latest_hash, latest_message) = periphery\n      .request(GetLatestCommit {\n        name: repo.name.clone(),\n        path: optional_string(&repo.config.path),\n      })\n      .await\n      .ok()\n      .flatten()\n      .map(|c| (c.hash, c.message))\n      .unzip();\n    status_cache\n      .insert(\n        repo.id,\n        CachedRepoStatus {\n          latest_hash,\n          latest_message,\n        }\n        .into(),\n      )\n      .await;\n  }\n}\n\nfn filter_volumes(\n  server: &Server,\n  mut stats: SystemStats,\n) -> SystemStats {\n  stats.disks.retain(|disk| {\n    // Always filter out volume mounts\n    !disk.mount.starts_with(\"/var/lib/docker/volumes\")\n    // Filter out any that were declared to ignore in server config\n      && !server\n        .config\n        .ignore_mounts\n        .iter()\n        .any(|mount| disk.mount.starts_with(mount))\n  });\n  stats\n}\n"
  },
  {
    "path": "bin/core/src/monitor/record.rs",
    "content": "use komodo_client::entities::stats::{\n  SystemStatsRecord, TotalDiskUsage, sum_disk_usage,\n};\n\nuse crate::state::{db_client, server_status_cache};\n\n#[instrument(level = \"debug\")]\npub async fn record_server_stats(ts: i64) {\n  let status = server_status_cache().get_list().await;\n  let records = status\n    .into_iter()\n    .filter_map(|status| {\n      let stats = status.stats.as_ref()?;\n\n      let TotalDiskUsage {\n        used_gb: disk_used_gb,\n        total_gb: disk_total_gb,\n      } = sum_disk_usage(&stats.disks);\n\n      Some(SystemStatsRecord {\n        ts,\n        sid: status.id.clone(),\n        cpu_perc: stats.cpu_perc,\n        load_average: stats.load_average.clone(),\n        mem_total_gb: stats.mem_total_gb,\n        mem_used_gb: stats.mem_used_gb,\n        disk_total_gb,\n        disk_used_gb,\n        disks: stats.disks.clone(),\n        network_ingress_bytes: stats.network_ingress_bytes,\n        network_egress_bytes: stats.network_egress_bytes,\n      })\n    })\n    .collect::<Vec<_>>();\n  if !records.is_empty() {\n    let res = db_client().stats.insert_many(records).await;\n    if let Err(e) = res {\n      error!(\"failed to record server stats | {e:#}\");\n    }\n  }\n}\n"
  },
  {
    "path": "bin/core/src/monitor/resources.rs",
    "content": "use std::{\n  collections::HashSet,\n  sync::{Mutex, OnceLock},\n};\n\nuse anyhow::Context;\nuse komodo_client::{\n  api::execute::{Deploy, DeployStack},\n  entities::{\n    ResourceTarget,\n    alert::{Alert, AlertData, SeverityLevel},\n    build::Build,\n    deployment::{Deployment, DeploymentImage, DeploymentState},\n    docker::{\n      container::{ContainerListItem, ContainerStateStatusEnum},\n      image::ImageListItem,\n    },\n    komodo_timestamp,\n    stack::{Stack, StackService, StackServiceNames, StackState},\n    user::auto_redeploy_user,\n  },\n};\n\nuse crate::{\n  alert::send_alerts,\n  api::execute::{self, ExecuteRequest},\n  helpers::query::get_stack_state_from_containers,\n  stack::{\n    compose_container_match_regex,\n    services::extract_services_from_stack,\n  },\n  state::{\n    action_states, db_client, deployment_status_cache,\n    stack_status_cache,\n  },\n};\n\nuse super::{CachedDeploymentStatus, CachedStackStatus, History};\n\nfn deployment_alert_sent_cache() -> &'static Mutex<HashSet<String>> {\n  static CACHE: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();\n  CACHE.get_or_init(Default::default)\n}\n\npub async fn update_deployment_cache(\n  server_name: String,\n  deployments: Vec<Deployment>,\n  containers: &[ContainerListItem],\n  images: &[ImageListItem],\n  builds: &[Build],\n) {\n  let deployment_status_cache = deployment_status_cache();\n  for deployment in deployments {\n    let container = containers\n      .iter()\n      .find(|container| container.name == deployment.name)\n      .cloned();\n    let prev = deployment_status_cache\n      .get(&deployment.id)\n      .await\n      .map(|s| s.curr.state);\n    let state = container\n      .as_ref()\n      .map(|c| c.state.into())\n      .unwrap_or(DeploymentState::NotDeployed);\n    let image = match deployment.config.image {\n      DeploymentImage::Build { build_id, version } => {\n        let (build_name, build_version) = builds\n          .iter()\n          .find(|build| build.id == build_id)\n          .map(|b| (b.name.as_ref(), b.config.version))\n          .unwrap_or((\"Unknown\", Default::default()));\n        let version = if version.is_none() {\n          build_version.to_string()\n        } else {\n          version.to_string()\n        };\n        format!(\"{build_name}:{version}\")\n      }\n      DeploymentImage::Image { image } => {\n        // If image already has tag, leave it,\n        // otherwise default the tag to latest\n        if image.contains(':') {\n          image.to_string()\n        } else {\n          format!(\"{image}:latest\")\n        }\n      }\n    };\n    let update_available = if let Some(ContainerListItem {\n      image_id: Some(curr_image_id),\n      ..\n    }) = &container\n    {\n      // Docker will automatically strip `docker.io` from incoming image names re #468.\n      // Need to strip it in order to match by image name and find available updates.\n      let image = image.strip_prefix(\"docker.io/\").unwrap_or(&image);\n      images\n        .iter()\n        .find(|i| i.name == image)\n        .map(|i| &i.id != curr_image_id)\n        .unwrap_or_default()\n    } else {\n      false\n    };\n\n    if update_available {\n      if deployment.config.auto_update {\n        if state == DeploymentState::Running\n          && !action_states()\n            .deployment\n            .get_or_insert_default(&deployment.id)\n            .await\n            .busy()\n            .unwrap_or(true)\n        {\n          let id = deployment.id.clone();\n          let server_name = server_name.clone();\n          tokio::spawn(async move {\n            match execute::inner_handler(\n              ExecuteRequest::Deploy(Deploy {\n                deployment: deployment.name.clone(),\n                stop_time: None,\n                stop_signal: None,\n              }),\n              auto_redeploy_user().to_owned(),\n            )\n            .await\n            {\n              Ok(_) => {\n                let ts = komodo_timestamp();\n                let alert = Alert {\n                  id: Default::default(),\n                  ts,\n                  resolved: true,\n                  resolved_ts: ts.into(),\n                  level: SeverityLevel::Ok,\n                  target: ResourceTarget::Deployment(id.clone()),\n                  data: AlertData::DeploymentAutoUpdated {\n                    id,\n                    name: deployment.name,\n                    server_name,\n                    server_id: deployment.config.server_id,\n                    image,\n                  },\n                };\n                let res = db_client().alerts.insert_one(&alert).await;\n                if let Err(e) = res {\n                  error!(\n                    \"Failed to record DeploymentAutoUpdated to db | {e:#}\"\n                  );\n                }\n                send_alerts(&[alert]).await;\n              }\n              Err(e) => {\n                warn!(\n                  \"Failed to auto update Deployment {} | {e:#}\",\n                  deployment.name\n                )\n              }\n            }\n          });\n        }\n      } else if state == DeploymentState::Running\n        && deployment.config.send_alerts\n        && !deployment_alert_sent_cache()\n          .lock()\n          .unwrap()\n          .contains(&deployment.id)\n      {\n        // Add that it is already sent to the cache, so another alert won't be sent.\n        deployment_alert_sent_cache()\n          .lock()\n          .unwrap()\n          .insert(deployment.id.clone());\n        let ts = komodo_timestamp();\n        let alert = Alert {\n          id: Default::default(),\n          ts,\n          resolved: true,\n          resolved_ts: ts.into(),\n          level: SeverityLevel::Ok,\n          target: ResourceTarget::Deployment(deployment.id.clone()),\n          data: AlertData::DeploymentImageUpdateAvailable {\n            id: deployment.id.clone(),\n            name: deployment.name,\n            server_name: server_name.clone(),\n            server_id: deployment.config.server_id,\n            image,\n          },\n        };\n        let res = db_client().alerts.insert_one(&alert).await;\n        if let Err(e) = res {\n          error!(\n            \"Failed to record DeploymentImageUpdateAvailable to db | {e:#}\"\n          );\n        }\n        send_alerts(&[alert]).await;\n      }\n    } else {\n      // If it sees there is no longer update available, remove\n      // from the sent cache, so on next `update_available = true`\n      // the cache is empty and a fresh alert will be sent.\n      deployment_alert_sent_cache()\n        .lock()\n        .unwrap()\n        .remove(&deployment.id);\n    }\n    deployment_status_cache\n      .insert(\n        deployment.id.clone(),\n        History {\n          curr: CachedDeploymentStatus {\n            id: deployment.id,\n            state,\n            container,\n            update_available,\n          },\n          prev,\n        }\n        .into(),\n      )\n      .await;\n  }\n}\n\n/// (StackId, Service)\nfn stack_alert_sent_cache()\n-> &'static Mutex<HashSet<(String, String)>> {\n  static CACHE: OnceLock<Mutex<HashSet<(String, String)>>> =\n    OnceLock::new();\n  CACHE.get_or_init(Default::default)\n}\n\npub async fn update_stack_cache(\n  server_name: String,\n  stacks: Vec<Stack>,\n  containers: &[ContainerListItem],\n  images: &[ImageListItem],\n) {\n  let stack_status_cache = stack_status_cache();\n  for stack in stacks {\n    let services = extract_services_from_stack(&stack);\n    let mut services_with_containers = services.iter().map(|StackServiceNames { service_name, container_name, image }| {\n      let container = containers.iter().find(|container| {\n        match compose_container_match_regex(container_name)\n          .with_context(|| format!(\"failed to construct container name matching regex for service {service_name}\")) \n        {\n          Ok(regex) => regex,\n          Err(e) => {\n            warn!(\"{e:#}\");\n            return false\n          }\n        }.is_match(&container.name)\n      }).cloned();\n      let image = if image.contains(':') {\n        image.to_string()\n      } else {\n        format!(\"{image}:latest\")\n      };\n      let update_available = if let Some(ContainerListItem { image_id: Some(curr_image_id), .. }) = &container {\n        // Docker will automatically strip `docker.io` from incoming image names re #468.\n        // Need to strip it in order to match by image tag and find available update.\n        let image =\n          image.strip_prefix(\"docker.io/\").unwrap_or(&image);\n        images\n          .iter()\n          .find(|i| i.name == image)\n          .map(|i| &i.id != curr_image_id)\n          .unwrap_or_default()\n      } else {\n        false\n      };\n      if update_available {\n        if !stack.config.auto_update\n          && stack.config.send_alerts\n          && container.is_some()\n          && container.as_ref().unwrap().state == ContainerStateStatusEnum::Running\n          && !stack_alert_sent_cache()\n            .lock()\n            .unwrap()\n            .contains(&(stack.id.clone(), service_name.clone()))\n        {\n          stack_alert_sent_cache()\n            .lock()\n            .unwrap()\n            .insert((stack.id.clone(), service_name.clone()));\n          let ts = komodo_timestamp();\n          let alert = Alert {\n            id: Default::default(),\n            ts,\n            resolved: true,\n            resolved_ts: ts.into(),\n            level: SeverityLevel::Ok,\n            target: ResourceTarget::Stack(stack.id.clone()),\n            data: AlertData::StackImageUpdateAvailable {\n              id: stack.id.clone(),\n              name: stack.name.clone(),\n              server_name: server_name.clone(),\n              server_id: stack.config.server_id.clone(),\n              service: service_name.clone(),\n              image: image.clone(),\n            },\n          };\n          tokio::spawn(async move {\n            let res = db_client().alerts.insert_one(&alert).await;\n            if let Err(e) = res {\n              error!(\n                \"Failed to record StackImageUpdateAvailable to db | {e:#}\"\n              );\n            }\n            send_alerts(&[alert]).await;\n          });\n        }\n      } else {\n        stack_alert_sent_cache()\n          .lock()\n          .unwrap()\n          .remove(&(stack.id.clone(), service_name.clone()));\n      }\n      StackService {\n        service: service_name.clone(),\n        image: image.clone(),\n        container,\n        update_available,\n      }\n    }).collect::<Vec<_>>();\n\n    let mut images_with_update = Vec::new();\n    let mut services_to_update = Vec::new();\n\n    for service in services_with_containers.iter() {\n      if service.update_available {\n        images_with_update.push(service.image.clone());\n        // Only allow it to actually trigger an auto update deploy\n        // if the service is running.\n        if service\n          .container\n          .as_ref()\n          .map(|c| c.state == ContainerStateStatusEnum::Running)\n          .unwrap_or_default()\n        {\n          services_to_update.push(service.service.clone());\n        }\n      }\n    }\n\n    let state = get_stack_state_from_containers(\n      &stack.config.ignore_services,\n      &services,\n      containers,\n    );\n    if !services_to_update.is_empty()\n      && stack.config.auto_update\n      && state == StackState::Running\n      && !action_states()\n        .stack\n        .get_or_insert_default(&stack.id)\n        .await\n        .busy()\n        .unwrap_or(true)\n    {\n      let id = stack.id.clone();\n      let server_name = server_name.clone();\n      let services = if stack.config.auto_update_all_services {\n        Vec::new()\n      } else {\n        services_to_update\n      };\n      tokio::spawn(async move {\n        match execute::inner_handler(\n          ExecuteRequest::DeployStack(DeployStack {\n            stack: stack.name.clone(),\n            services,\n            stop_time: None,\n          }),\n          auto_redeploy_user().to_owned(),\n        )\n        .await\n        {\n          Ok(_) => {\n            let ts = komodo_timestamp();\n            let alert = Alert {\n              id: Default::default(),\n              ts,\n              resolved: true,\n              resolved_ts: ts.into(),\n              level: SeverityLevel::Ok,\n              target: ResourceTarget::Stack(id.clone()),\n              data: AlertData::StackAutoUpdated {\n                id,\n                name: stack.name.clone(),\n                server_name,\n                server_id: stack.config.server_id,\n                images: images_with_update,\n              },\n            };\n            let res = db_client().alerts.insert_one(&alert).await;\n            if let Err(e) = res {\n              error!(\n                \"Failed to record StackAutoUpdated to db | {e:#}\"\n              );\n            }\n            send_alerts(&[alert]).await;\n          }\n          Err(e) => {\n            warn!(\"Failed auto update Stack {} | {e:#}\", stack.name,)\n          }\n        }\n      });\n    }\n    services_with_containers\n      .sort_by(|a, b| a.service.cmp(&b.service));\n    let prev = stack_status_cache\n      .get(&stack.id)\n      .await\n      .map(|s| s.curr.state);\n    let status = CachedStackStatus {\n      id: stack.id.clone(),\n      state,\n      services: services_with_containers,\n    };\n    stack_status_cache\n      .insert(stack.id, History { curr: status, prev }.into())\n      .await;\n  }\n}\n"
  },
  {
    "path": "bin/core/src/network.rs",
    "content": "//! # Network Configuration Module\n//!\n//! This module provides manual network interface configuration for multi-NIC Docker environments.\n//! It allows Komodo Core to specify which network interface should be used as the default route\n//! for internet traffic, which is particularly useful in complex networking setups with multiple\n//! network interfaces.\n//!\n//! ## Features\n//! - Automatic container environment detection\n//! - Interface validation (existence and UP state)\n//! - Gateway discovery from routing tables or network configuration\n//! - Safe default route modification with privilege checking\n//! - Comprehensive error handling and logging\n\nuse anyhow::{Context, anyhow};\nuse tokio::process::Command;\nuse tracing::{debug, info, trace, warn};\n\n/// Standard gateway addresses to test for Docker networks\nconst DOCKER_GATEWAY_CANDIDATES: &[&str] = &[\".1\", \".254\"];\n\n/// Container environment detection files\nconst DOCKERENV_FILE: &str = \"/.dockerenv\";\nconst CGROUP_FILE: &str = \"/proc/1/cgroup\";\n\n/// Check if running in container environment\nfn is_container_environment() -> bool {\n  // Check for Docker-specific indicators\n  if std::path::Path::new(DOCKERENV_FILE).exists() {\n    return true;\n  }\n\n  // Check container environment variable\n  if std::env::var(\"container\").is_ok() {\n    return true;\n  }\n\n  // Check cgroup for container runtime indicators\n  if let Ok(content) = std::fs::read_to_string(CGROUP_FILE)\n    && (content.contains(\"docker\") || content.contains(\"containerd\"))\n  {\n    return true;\n  }\n\n  false\n}\n\n/// Configure internet gateway for specified interface\npub async fn configure_internet_gateway() {\n  use crate::config::core_config;\n\n  let config = core_config();\n\n  if !is_container_environment() {\n    debug!(\"Not in container, skipping network configuration\");\n    return;\n  }\n\n  if !config.internet_interface.is_empty() {\n    debug!(\n      \"Configuring internet interface: {}\",\n      config.internet_interface\n    );\n    if let Err(e) =\n      configure_manual_interface(&config.internet_interface).await\n    {\n      warn!(\"Failed to configure internet gateway: {e:#}\");\n    }\n  } else {\n    debug!(\"No interface specified, using default routing\");\n  }\n}\n\n/// Configure interface as default route\nasync fn configure_manual_interface(\n  interface_name: &str,\n) -> anyhow::Result<()> {\n  // Verify interface exists and is up\n  let interface_check = Command::new(\"ip\")\n    .args([\"addr\", \"show\", interface_name])\n    .output()\n    .await\n    .context(\"Failed to check interface status\")?;\n\n  if !interface_check.status.success() {\n    return Err(anyhow!(\n      \"Interface '{}' does not exist or is not accessible. Available interfaces can be listed with 'ip addr show'\",\n      interface_name\n    ));\n  }\n\n  let interface_info =\n    String::from_utf8_lossy(&interface_check.stdout);\n  if !interface_info.contains(\"state UP\") {\n    return Err(anyhow!(\n      \"Interface '{}' is not UP. Please ensure the interface is enabled and connected\",\n      interface_name\n    ));\n  }\n\n  debug!(\"Interface {} is UP\", interface_name);\n\n  let gateway = find_gateway(interface_name).await?;\n  debug!(\"Found gateway {} for {}\", gateway, interface_name);\n\n  set_default_gateway(&gateway, interface_name).await?;\n  info!(\n    \"🌐 Configured {} as default gateway via {}\",\n    interface_name, gateway\n  );\n  Ok(())\n}\n\n/// Find gateway for interface\nasync fn find_gateway(\n  interface_name: &str,\n) -> anyhow::Result<String> {\n  // Get interface IP address\n  let addr_output = Command::new(\"ip\")\n    .args([\"addr\", \"show\", interface_name])\n    .output()\n    .await\n    .context(\"Failed to get interface address\")?;\n\n  let addr_info = String::from_utf8_lossy(&addr_output.stdout);\n  let mut ip_cidr = None;\n\n  // Extract IP/CIDR from interface info\n  for line in addr_info.lines() {\n    if line.trim().starts_with(\"inet \") && !line.contains(\"127.0.0.1\")\n    {\n      let parts: Vec<&str> = line.split_whitespace().collect();\n      if let Some(found_ip_cidr) = parts.get(1) {\n        debug!(\n          \"Interface {} has IP {}\",\n          interface_name, found_ip_cidr\n        );\n        ip_cidr = Some(*found_ip_cidr);\n        break;\n      }\n    }\n  }\n\n  let ip_cidr = ip_cidr.ok_or_else(|| anyhow!(\n        \"Could not find IP address for interface '{}'. Ensure interface has a valid IPv4 address\",\n        interface_name\n    ))?;\n\n  trace!(\n    \"Finding gateway for interface {} in network {}\",\n    interface_name, ip_cidr\n  );\n\n  // Try to find gateway from routing table\n  let route_output = Command::new(\"ip\")\n    .args([\"route\", \"show\", \"dev\", interface_name])\n    .output()\n    .await\n    .context(\"Failed to get routes for interface\")?;\n\n  if route_output.status.success() {\n    let routes = String::from_utf8(route_output.stdout)?;\n    trace!(\"Routes for {}: {}\", interface_name, routes.trim());\n\n    // Look for routes with gateway\n    for line in routes.lines() {\n      if line.contains(\"via\") {\n        let parts: Vec<&str> = line.split_whitespace().collect();\n        if let Some(via_idx) = parts.iter().position(|&x| x == \"via\")\n          && let Some(&gateway) = parts.get(via_idx + 1)\n        {\n          trace!(\n            \"Found gateway {} for {} from routing table\",\n            gateway, interface_name\n          );\n          return Ok(gateway.to_string());\n        }\n      }\n    }\n  }\n\n  // Derive gateway from network configuration (Docker standard: .1)\n  if let Some(network_base) = ip_cidr.split('/').next() {\n    let ip_parts: Vec<&str> = network_base.split('.').collect();\n    if ip_parts.len() == 4 {\n      let potential_gateways: Vec<String> = DOCKER_GATEWAY_CANDIDATES\n        .iter()\n        .map(|suffix| {\n          format!(\n            \"{}.{}.{}{}\",\n            ip_parts[0], ip_parts[1], ip_parts[2], suffix\n          )\n        })\n        .collect();\n\n      for gateway in potential_gateways {\n        trace!(\n          \"Testing potential gateway {} for {}\",\n          gateway, interface_name\n        );\n\n        // Check if gateway is reachable\n        let route_test = Command::new(\"ip\")\n          .args([\"route\", \"get\", &gateway, \"dev\", interface_name])\n          .output()\n          .await;\n\n        if let Ok(output) = route_test\n          && output.status.success()\n        {\n          trace!(\n            \"Gateway {} is reachable via {}\",\n            gateway, interface_name\n          );\n          return Ok(gateway.to_string());\n        }\n\n        // Fallback: assume .1 is gateway (Docker standard)\n        if gateway.ends_with(\".1\") {\n          trace!(\n            \"Assuming Docker gateway {} for {}\",\n            gateway, interface_name\n          );\n          return Ok(gateway.to_string());\n        }\n      }\n    }\n  }\n\n  Err(anyhow!(\n    \"Could not determine gateway for interface '{}' in network '{}'. \\\n        Ensure the interface is properly configured with a valid gateway\",\n    interface_name,\n    ip_cidr\n  ))\n}\n\n/// Set default gateway to use specified interface\nasync fn set_default_gateway(\n  gateway: &str,\n  interface_name: &str,\n) -> anyhow::Result<()> {\n  trace!(\n    \"Setting default gateway to {} via {}\",\n    gateway, interface_name\n  );\n\n  // Check if we have network privileges\n  if !check_network_privileges().await {\n    warn!(\n      \"⚠️  Container lacks network privileges (NET_ADMIN capability required)\"\n    );\n    warn!(\n      \"Add 'cap_add: [\\\"NET_ADMIN\\\"]' to your docker-compose.yaml\"\n    );\n    return Err(anyhow!(\n      \"Insufficient network privileges to modify routing table. \\\n            Container needs NET_ADMIN capability to configure network interfaces\"\n    ));\n  }\n\n  // Remove existing default routes\n  let remove_default = Command::new(\"sh\")\n    .args([\"-c\", \"ip route del default 2>/dev/null || true\"])\n    .output()\n    .await;\n\n  if let Ok(output) = remove_default\n    && output.status.success()\n  {\n    trace!(\"Removed existing default routes\");\n  }\n\n  // Add new default route\n  let add_default_cmd = format!(\n    \"ip route add default via {gateway} dev {interface_name}\"\n  );\n  trace!(\"Adding default route: {}\", add_default_cmd);\n\n  let add_default = Command::new(\"sh\")\n    .args([\"-c\", &add_default_cmd])\n    .output()\n    .await\n    .context(\"Failed to add default route\")?;\n\n  if !add_default.status.success() {\n    let error = String::from_utf8_lossy(&add_default.stderr)\n      .trim()\n      .to_string();\n    return Err(anyhow!(\n      \"❌ Failed to set default gateway via '{}': {}. \\\n            Verify interface configuration and network permissions\",\n      interface_name,\n      error\n    ));\n  }\n\n  trace!(\"Default gateway set to {} via {}\", gateway, interface_name);\n  Ok(())\n}\n\n/// Check if we have sufficient network privileges\nasync fn check_network_privileges() -> bool {\n  // Try to test NET_ADMIN capability with a harmless route operation\n  let capability_test = Command::new(\"sh\")\n        .args([\"-c\", \"ip route add 198.51.100.1/32 dev lo 2>/dev/null && ip route del 198.51.100.1/32 dev lo 2>/dev/null\"])\n        .output()\n        .await;\n\n  matches!(capability_test, Ok(output) if output.status.success())\n}\n"
  },
  {
    "path": "bin/core/src/permission.rs",
    "content": "use std::collections::HashSet;\n\nuse anyhow::{Context, anyhow};\nuse database::mongo_indexed::doc;\nuse database::mungos::find::find_collect;\nuse futures::{FutureExt, future::BoxFuture};\nuse indexmap::IndexSet;\nuse komodo_client::{\n  api::read::GetPermission,\n  entities::{\n    permission::{PermissionLevel, PermissionLevelAndSpecifics},\n    resource::Resource,\n    user::User,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  api::read::ReadArgs,\n  config::core_config,\n  helpers::query::{get_user_user_groups, user_target_query},\n  resource::{KomodoResource, get},\n  state::db_client,\n};\n\npub async fn get_check_permissions<T: KomodoResource>(\n  id_or_name: &str,\n  user: &User,\n  required_permissions: PermissionLevelAndSpecifics,\n) -> anyhow::Result<Resource<T::Config, T::Info>> {\n  let resource = get::<T>(id_or_name).await?;\n\n  // Allow all if admin\n  if user.admin {\n    return Ok(resource);\n  }\n\n  let user_permissions =\n    get_user_permission_on_resource::<T>(user, &resource.id).await?;\n\n  if (\n    // Allow if its just read or below, and transparent mode enabled\n    (required_permissions.level <= PermissionLevel::Read && core_config().transparent_mode)\n    // Allow if resource has base permission level greater than or equal to required permission level\n    || resource.base_permission.level >= required_permissions.level\n  ) && user_permissions\n    .fulfills_specific(&required_permissions.specific)\n  {\n    return Ok(resource);\n  }\n\n  if user_permissions.fulfills(&required_permissions) {\n    Ok(resource)\n  } else {\n    Err(anyhow!(\n      \"User does not have required permissions on this {}. Must have at least {} permissions{}\",\n      T::resource_type(),\n      required_permissions.level,\n      if required_permissions.specific.is_empty() {\n        String::new()\n      } else {\n        format!(\n          \", as well as these specific permissions: [{}]\",\n          required_permissions.specifics_for_log()\n        )\n      }\n    ))\n  }\n}\n\n#[instrument(level = \"debug\")]\npub fn get_user_permission_on_resource<'a, T: KomodoResource>(\n  user: &'a User,\n  resource_id: &'a str,\n) -> BoxFuture<'a, anyhow::Result<PermissionLevelAndSpecifics>> {\n  Box::pin(async {\n    // Admin returns early with max permissions\n    if user.admin {\n      return Ok(PermissionLevel::Write.all());\n    }\n\n    let resource_type = T::resource_type();\n    let resource = get::<T>(resource_id).await?;\n    let initial_specific = if let Some(additional_target) =\n      T::inherit_specific_permissions_from(&resource)\n      // Ensure target is actually assigned\n      && !additional_target.is_empty()\n    {\n      GetPermission {\n        target: additional_target,\n      }\n      .resolve(&ReadArgs { user: user.clone() })\n      .await\n      .map_err(|e| e.error)\n      .context(\"failed to get user permission on additional target\")?\n      .specific\n    } else {\n      IndexSet::new()\n    };\n\n    let mut permission = PermissionLevelAndSpecifics {\n      level: if core_config().transparent_mode {\n        PermissionLevel::Read\n      } else {\n        PermissionLevel::None\n      },\n      specific: initial_specific,\n    };\n\n    // Add in the resource level global base permissions\n    if resource.base_permission.level > permission.level {\n      permission.level = resource.base_permission.level;\n    }\n    permission\n      .specific\n      .extend(resource.base_permission.specific);\n\n    // Overlay users base on resource variant\n    if let Some(user_permission) =\n      user.all.get(&resource_type).cloned()\n    {\n      if user_permission.level > permission.level {\n        permission.level = user_permission.level;\n      }\n      permission.specific.extend(user_permission.specific);\n    }\n\n    // Overlay any user groups base on resource variant\n    let groups = get_user_user_groups(&user.id).await?;\n    for group in &groups {\n      if let Some(group_permission) =\n        group.all.get(&resource_type).cloned()\n      {\n        if group_permission.level > permission.level {\n          permission.level = group_permission.level;\n        }\n        permission.specific.extend(group_permission.specific);\n      }\n    }\n\n    // Overlay any specific permissions\n    let permission = find_collect(\n      &db_client().permissions,\n      doc! {\n        \"$or\": user_target_query(&user.id, &groups)?,\n        \"resource_target.type\": resource_type.as_ref(),\n        \"resource_target.id\": resource_id\n      },\n      None,\n    )\n    .await\n    .context(\"failed to query db for permissions\")?\n    .into_iter()\n    // get the max resource permission user has between personal / any user groups\n    .fold(permission, |mut permission, resource_permission| {\n      if resource_permission.level > permission.level {\n        permission.level = resource_permission.level\n      }\n      permission.specific.extend(resource_permission.specific);\n      permission\n    });\n    Ok(permission)\n  })\n}\n\n/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access).\n#[instrument(level = \"debug\")]\npub async fn get_resource_ids_for_user<T: KomodoResource>(\n  user: &User,\n) -> anyhow::Result<Option<Vec<String>>> {\n  // Check admin or transparent mode\n  if user.admin || core_config().transparent_mode {\n    return Ok(None);\n  }\n\n  let resource_type = T::resource_type();\n\n  // Check user 'all' on variant\n  if let Some(permission) = user.all.get(&resource_type).cloned()\n    && permission.level > PermissionLevel::None\n  {\n    return Ok(None);\n  }\n\n  // Check user groups 'all' on variant\n  let groups = get_user_user_groups(&user.id).await?;\n  for group in &groups {\n    if let Some(permission) = group.all.get(&resource_type).cloned()\n      && permission.level > PermissionLevel::None\n    {\n      return Ok(None);\n    }\n  }\n\n  let (base, perms) = tokio::try_join!(\n    // Get any resources with non-none base permission,\n    find_collect(\n      T::coll(),\n      doc! { \"$or\": [\n        { \"base_permission\": { \"$in\": [\"Read\", \"Execute\", \"Write\"] } },\n        { \"base_permission.level\": { \"$in\": [\"Read\", \"Execute\", \"Write\"] } }\n      ] },\n      None,\n    )\n    .map(|res| res.with_context(|| format!(\n      \"failed to query {resource_type} on db\"\n    ))),\n    // And any ids using the permissions table\n    find_collect(\n      &db_client().permissions,\n      doc! {\n        \"$or\": user_target_query(&user.id, &groups)?,\n        \"resource_target.type\": resource_type.as_ref(),\n        \"level\": { \"$in\": [\"Read\", \"Execute\", \"Write\"] }\n      },\n      None,\n    )\n    .map(|res| res.context(\"failed to query permissions on db\"))\n  )?;\n\n  // Add specific ids\n  let ids = perms\n    .into_iter()\n    .map(|p| p.resource_target.extract_variant_id().1.to_string())\n    // Chain in the ones with non-None base permissions\n    .chain(base.into_iter().map(|res| res.id))\n    // collect into hashset first to remove any duplicates\n    .collect::<HashSet<_>>();\n\n  Ok(Some(ids.into_iter().collect()))\n}\n"
  },
  {
    "path": "bin/core/src/resource/action.rs",
    "content": "use std::time::Duration;\n\nuse anyhow::Context;\nuse database::mungos::{\n  find::find_collect,\n  mongodb::{Collection, bson::doc, options::FindOneOptions},\n};\nuse komodo_client::entities::{\n  NoData, Operation, ResourceTarget, ResourceTargetVariant,\n  action::{\n    Action, ActionConfig, ActionConfigDiff, ActionListItem,\n    ActionListItemInfo, ActionQuerySpecifics, ActionState,\n    PartialActionConfig,\n  },\n  resource::Resource,\n  update::Update,\n  user::User,\n};\n\nuse crate::{\n  helpers::query::{get_action_state, get_last_run_at},\n  schedule::{\n    cancel_schedule, get_schedule_item_info, update_schedule,\n  },\n  state::{action_state_cache, action_states, db_client},\n};\n\nimpl super::KomodoResource for Action {\n  type Config = ActionConfig;\n  type PartialConfig = PartialActionConfig;\n  type ConfigDiff = ActionConfigDiff;\n  type Info = NoData;\n  type ListItem = ActionListItem;\n  type QuerySpecifics = ActionQuerySpecifics;\n\n  fn resource_type() -> ResourceTargetVariant {\n    ResourceTargetVariant::Action\n  }\n\n  fn resource_target(id: impl Into<String>) -> ResourceTarget {\n    ResourceTarget::Action(id.into())\n  }\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>\n  {\n    &db_client().actions\n  }\n\n  async fn to_list_item(\n    action: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem {\n    let (state, last_run_at) = tokio::join!(\n      get_action_state(&action.id),\n      get_last_run_at::<Action>(&action.id)\n    );\n    let (next_scheduled_run, schedule_error) = get_schedule_item_info(\n      &ResourceTarget::Action(action.id.clone()),\n    );\n    ActionListItem {\n      name: action.name,\n      id: action.id,\n      template: action.template,\n      tags: action.tags,\n      resource_type: ResourceTargetVariant::Action,\n      info: ActionListItemInfo {\n        state,\n        last_run_at: last_run_at.unwrap_or(None),\n        next_scheduled_run,\n        schedule_error,\n      },\n    }\n  }\n\n  async fn busy(id: &String) -> anyhow::Result<bool> {\n    action_states()\n      .action\n      .get(id)\n      .await\n      .unwrap_or_default()\n      .busy()\n  }\n\n  // CREATE\n\n  fn create_operation() -> Operation {\n    Operation::CreateAction\n  }\n\n  fn user_can_create(user: &User) -> bool {\n    user.admin\n  }\n\n  async fn validate_create_config(\n    config: &mut Self::PartialConfig,\n    _user: &User,\n  ) -> anyhow::Result<()> {\n    if config.file_contents.is_none() {\n      config.file_contents =\n        Some(DEFAULT_ACTION_FILE_CONTENTS.to_string());\n    }\n    Ok(())\n  }\n\n  async fn post_create(\n    created: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    update_schedule(created);\n    refresh_action_state_cache().await;\n    Ok(())\n  }\n\n  // UPDATE\n\n  fn update_operation() -> Operation {\n    Operation::UpdateAction\n  }\n\n  async fn validate_update_config(\n    _id: &str,\n    _config: &mut Self::PartialConfig,\n    _user: &User,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  async fn post_update(\n    updated: &Self,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Self::post_create(updated, update).await\n  }\n\n  // RENAME\n\n  fn rename_operation() -> Operation {\n    Operation::RenameAction\n  }\n\n  // DELETE\n\n  fn delete_operation() -> Operation {\n    Operation::DeleteAction\n  }\n\n  async fn pre_delete(\n    _resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  async fn post_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    cancel_schedule(&ResourceTarget::Action(resource.id.clone()));\n    action_state_cache().remove(&resource.id).await;\n    Ok(())\n  }\n}\n\npub fn spawn_action_state_refresh_loop() {\n  tokio::spawn(async move {\n    loop {\n      refresh_action_state_cache().await;\n      tokio::time::sleep(Duration::from_secs(60)).await;\n    }\n  });\n}\n\npub async fn refresh_action_state_cache() {\n  let _ = async {\n    let actions = find_collect(&db_client().actions, None, None)\n      .await\n      .context(\"Failed to get Actions from db\")?;\n    let cache = action_state_cache();\n    for action in actions {\n      let state = get_action_state_from_db(&action.id).await;\n      cache.insert(action.id, state).await;\n    }\n    anyhow::Ok(())\n  }\n  .await\n  .inspect_err(|e| {\n    error!(\"Failed to refresh Action state cache | {e:#}\")\n  });\n}\n\nasync fn get_action_state_from_db(id: &str) -> ActionState {\n  async {\n    let state = db_client()\n      .updates\n      .find_one(doc! {\n        \"target.type\": \"Action\",\n        \"target.id\": id,\n        \"operation\": \"RunAction\"\n      })\n      .with_options(\n        FindOneOptions::builder()\n          .sort(doc! { \"start_ts\": -1 })\n          .build(),\n      )\n      .await?\n      .map(|u| {\n        if u.success {\n          ActionState::Ok\n        } else {\n          ActionState::Failed\n        }\n      })\n      .unwrap_or(ActionState::Ok);\n    anyhow::Ok(state)\n  }\n  .await\n  .inspect_err(|e| {\n    warn!(\"Failed to get Action state for {id} | {e:#}\")\n  })\n  .unwrap_or(ActionState::Unknown)\n}\n\nconst DEFAULT_ACTION_FILE_CONTENTS: &str =\n  \"// Run actions using the pre initialized 'komodo' client.\nconst version: Types.GetVersionResponse = await komodo.read('GetVersion', {});\nconsole.log('🦎 Komodo version:', version.version, '🦎\\\\n');\n\n// Access arguments using the 'ARGS' object.\nconsole.log(ARGS);\";\n"
  },
  {
    "path": "bin/core/src/resource/alerter.rs",
    "content": "use database::mungos::mongodb::Collection;\nuse derive_variants::ExtractVariant;\nuse komodo_client::entities::{\n  Operation, ResourceTarget, ResourceTargetVariant,\n  alerter::{\n    Alerter, AlerterConfig, AlerterConfigDiff, AlerterListItem,\n    AlerterListItemInfo, AlerterQuerySpecifics, PartialAlerterConfig,\n  },\n  resource::Resource,\n  update::Update,\n  user::User,\n};\n\nuse crate::state::db_client;\n\nimpl super::KomodoResource for Alerter {\n  type Config = AlerterConfig;\n  type PartialConfig = PartialAlerterConfig;\n  type ConfigDiff = AlerterConfigDiff;\n  type Info = ();\n  type ListItem = AlerterListItem;\n  type QuerySpecifics = AlerterQuerySpecifics;\n\n  fn resource_type() -> ResourceTargetVariant {\n    ResourceTargetVariant::Alerter\n  }\n\n  fn resource_target(id: impl Into<String>) -> ResourceTarget {\n    ResourceTarget::Alerter(id.into())\n  }\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>\n  {\n    &db_client().alerters\n  }\n\n  async fn to_list_item(\n    alerter: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem {\n    AlerterListItem {\n      name: alerter.name,\n      id: alerter.id,\n      template: alerter.template,\n      tags: alerter.tags,\n      resource_type: ResourceTargetVariant::Alerter,\n      info: AlerterListItemInfo {\n        endpoint_type: alerter.config.endpoint.extract_variant(),\n        enabled: alerter.config.enabled,\n      },\n    }\n  }\n\n  async fn busy(_id: &String) -> anyhow::Result<bool> {\n    Ok(false)\n  }\n\n  // CREATE\n\n  fn create_operation() -> Operation {\n    Operation::CreateAlerter\n  }\n\n  fn user_can_create(user: &User) -> bool {\n    user.admin\n  }\n\n  async fn validate_create_config(\n    _config: &mut Self::PartialConfig,\n    _user: &User,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  async fn post_create(\n    _created: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  // UPDATE\n\n  fn update_operation() -> Operation {\n    Operation::UpdateAlerter\n  }\n\n  async fn validate_update_config(\n    _id: &str,\n    _config: &mut Self::PartialConfig,\n    _user: &User,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  async fn post_update(\n    _updated: &Self,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  // RENAME\n\n  fn rename_operation() -> Operation {\n    Operation::RenameAlerter\n  }\n\n  // DELETE\n\n  fn delete_operation() -> Operation {\n    Operation::DeleteAlerter\n  }\n\n  async fn pre_delete(\n    _resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  async fn post_delete(\n    _resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "bin/core/src/resource/build.rs",
    "content": "use std::time::Duration;\n\nuse anyhow::Context;\nuse database::mungos::{\n  find::find_collect,\n  mongodb::{Collection, bson::doc, options::FindOptions},\n};\nuse formatting::format_serror;\nuse komodo_client::{\n  api::write::RefreshBuildCache,\n  entities::{\n    Operation, ResourceTarget, ResourceTargetVariant,\n    build::{\n      Build, BuildConfig, BuildConfigDiff, BuildInfo, BuildListItem,\n      BuildListItemInfo, BuildQuerySpecifics, BuildState,\n      PartialBuildConfig,\n    },\n    builder::Builder,\n    environment_vars_from_str, optional_string,\n    permission::PermissionLevel,\n    repo::Repo,\n    resource::Resource,\n    to_docker_compatible_name,\n    update::Update,\n    user::{User, build_user},\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  api::write::WriteArgs,\n  config::core_config,\n  helpers::{\n    empty_or_only_spaces, query::get_latest_update, repo_link,\n  },\n  permission::get_check_permissions,\n  state::{\n    action_states, all_resources_cache, build_state_cache, db_client,\n  },\n};\n\nimpl super::KomodoResource for Build {\n  type Config = BuildConfig;\n  type PartialConfig = PartialBuildConfig;\n  type ConfigDiff = BuildConfigDiff;\n  type Info = BuildInfo;\n  type ListItem = BuildListItem;\n  type QuerySpecifics = BuildQuerySpecifics;\n\n  fn resource_type() -> ResourceTargetVariant {\n    ResourceTargetVariant::Build\n  }\n\n  fn resource_target(id: impl Into<String>) -> ResourceTarget {\n    ResourceTarget::Build(id.into())\n  }\n\n  fn validated_name(name: &str) -> String {\n    to_docker_compatible_name(name)\n  }\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>\n  {\n    &db_client().builds\n  }\n\n  async fn to_list_item(\n    build: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem {\n    let state = get_build_state(&build.id).await;\n\n    let default_git = (\n      build.config.git_provider,\n      build.config.repo,\n      build.config.branch,\n      build.config.git_https,\n    );\n    let (git_provider, repo, branch, git_https) =\n      if build.config.linked_repo.is_empty() {\n        default_git\n      } else {\n        all_resources_cache()\n          .load()\n          .repos\n          .get(&build.config.linked_repo)\n          .map(|r| {\n            (\n              r.config.git_provider.clone(),\n              r.config.repo.clone(),\n              r.config.branch.clone(),\n              r.config.git_https,\n            )\n          })\n          .unwrap_or(default_git)\n      };\n\n    BuildListItem {\n      name: build.name,\n      id: build.id,\n      template: build.template,\n      tags: build.tags,\n      resource_type: ResourceTargetVariant::Build,\n      info: BuildListItemInfo {\n        last_built_at: build.info.last_built_at,\n        version: build.config.version,\n        builder_id: build.config.builder_id,\n        files_on_host: build.config.files_on_host,\n        dockerfile_contents: !build.config.dockerfile.is_empty(),\n        linked_repo: build.config.linked_repo,\n        repo_link: repo_link(\n          &git_provider,\n          &repo,\n          &branch,\n          git_https,\n        ),\n        git_provider,\n        repo,\n        branch,\n        image_registry_domain: build\n          .config\n          .image_registry\n          .first()\n          .and_then(|r| optional_string(&r.domain)),\n        built_hash: build.info.built_hash,\n        latest_hash: build.info.latest_hash,\n        state,\n      },\n    }\n  }\n\n  async fn busy(id: &String) -> anyhow::Result<bool> {\n    action_states()\n      .build\n      .get(id)\n      .await\n      .unwrap_or_default()\n      .busy()\n  }\n\n  // CREATE\n\n  fn create_operation() -> Operation {\n    Operation::CreateBuild\n  }\n\n  fn user_can_create(user: &User) -> bool {\n    user.admin\n      || (!core_config().disable_non_admin_create\n        && user.create_build_permissions)\n  }\n\n  async fn validate_create_config(\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_create(\n    created: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    refresh_build_state_cache().await;\n    if let Err(e) = (RefreshBuildCache {\n      build: created.name.clone(),\n    })\n    .resolve(&WriteArgs {\n      user: build_user().to_owned(),\n    })\n    .await\n    {\n      update.push_error_log(\n        \"Refresh build cache\",\n        format_serror(&e.error.context(\"The build cache has failed to refresh. This may be due to a misconfiguration of the Build\").into())\n      );\n    };\n    Ok(())\n  }\n\n  // UPDATE\n\n  fn update_operation() -> Operation {\n    Operation::UpdateBuild\n  }\n\n  async fn validate_update_config(\n    _id: &str,\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_update(\n    updated: &Self,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Self::post_create(updated, update).await\n  }\n\n  // RENAME\n\n  fn rename_operation() -> Operation {\n    Operation::RenameBuild\n  }\n\n  // DELETE\n\n  fn delete_operation() -> Operation {\n    Operation::DeleteBuild\n  }\n\n  async fn pre_delete(\n    _resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  async fn post_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    build_state_cache().remove(&resource.id).await;\n    Ok(())\n  }\n}\n\npub fn spawn_build_state_refresh_loop() {\n  tokio::spawn(async move {\n    loop {\n      refresh_build_state_cache().await;\n      tokio::time::sleep(Duration::from_secs(60)).await;\n    }\n  });\n}\n\npub async fn refresh_build_state_cache() {\n  let _ = async {\n    let builds = find_collect(&db_client().builds, None, None)\n      .await\n      .context(\"failed to get builds from db\")?;\n    let cache = build_state_cache();\n    for build in builds {\n      let state = get_build_state_from_db(&build.id).await;\n      cache.insert(build.id, state).await;\n    }\n    anyhow::Ok(())\n  }\n  .await\n  .inspect_err(|e| {\n    error!(\"failed to refresh build state cache | {e:#}\")\n  });\n}\n\n#[instrument(skip(user))]\nasync fn validate_config(\n  config: &mut PartialBuildConfig,\n  user: &User,\n) -> anyhow::Result<()> {\n  if let Some(builder_id) = &config.builder_id\n    && !builder_id.is_empty()\n  {\n    let builder = super::get_check_permissions::<Builder>(\n      builder_id,\n      user,\n      PermissionLevel::Read.attach(),\n    )\n    .await\n    .context(\"Cannot attach Build to this Builder\")?;\n    config.builder_id = Some(builder.id)\n  }\n  if let Some(linked_repo) = &config.linked_repo\n    && !linked_repo.is_empty()\n  {\n    let repo = get_check_permissions::<Repo>(\n      linked_repo,\n      user,\n      PermissionLevel::Read.attach(),\n    )\n    .await\n    .context(\"Cannot attach Repo to this Build\")?;\n    // in case it comes in as name\n    config.linked_repo = Some(repo.id);\n  }\n  if let Some(build_args) = &config.build_args {\n    environment_vars_from_str(build_args)\n      .context(\"Invalid build_args\")?;\n  }\n  if let Some(secret_args) = &config.secret_args {\n    environment_vars_from_str(secret_args)\n      .context(\"Invalid secret_args\")?;\n  }\n  if let Some(extra_args) = &mut config.extra_args {\n    extra_args.retain(|v| !empty_or_only_spaces(v))\n  }\n  Ok(())\n}\n\nasync fn get_build_state(id: &String) -> BuildState {\n  if action_states()\n    .build\n    .get(id)\n    .await\n    .map(|s| s.get().map(|s| s.building))\n    .transpose()\n    .ok()\n    .flatten()\n    .unwrap_or_default()\n  {\n    return BuildState::Building;\n  }\n  build_state_cache().get(id).await.unwrap_or_default()\n}\n\nasync fn get_build_state_from_db(id: &str) -> BuildState {\n  async {\n    let state = match tokio::try_join!(\n      latest_2_build_updates(id),\n      get_latest_update(\n        ResourceTargetVariant::Build,\n        id,\n        Operation::CancelBuild\n      ),\n    )? {\n      ([Some(build), second], Some(cancel))\n        if cancel.start_ts > build.start_ts =>\n      {\n        match second {\n          Some(build) => {\n            if build.success {\n              BuildState::Ok\n            } else {\n              BuildState::Failed\n            }\n          }\n          None => BuildState::Ok,\n        }\n      }\n      ([Some(build), _], _) => {\n        if build.success {\n          BuildState::Ok\n        } else {\n          BuildState::Failed\n        }\n      }\n      _ => {\n        // No build update ever, should be fine\n        BuildState::Ok\n      }\n    };\n    anyhow::Ok(state)\n  }\n  .await\n  .inspect_err(|e| {\n    warn!(\"failed to get build state for {id} | {e:#}\")\n  })\n  .unwrap_or(BuildState::Unknown)\n}\n\nasync fn latest_2_build_updates(\n  id: &str,\n) -> anyhow::Result<[Option<Update>; 2]> {\n  let mut builds = find_collect(\n    &db_client().updates,\n    doc! {\n      \"target.type\": \"Build\",\n      \"target.id\": id,\n      \"operation\": \"RunBuild\"\n    },\n    FindOptions::builder()\n      .sort(doc! { \"start_ts\": -1 })\n      .limit(2)\n      .build(),\n  )\n  .await\n  .context(\"failed to query for latest updates\")?;\n  let second = builds.pop();\n  let first = builds.pop();\n  Ok([first, second])\n}\n"
  },
  {
    "path": "bin/core/src/resource/builder.rs",
    "content": "use anyhow::Context;\nuse database::mungos::mongodb::{\n  Collection,\n  bson::{Document, doc, to_document},\n};\nuse indexmap::IndexSet;\nuse komodo_client::entities::{\n  MergePartial, Operation, ResourceTarget, ResourceTargetVariant,\n  builder::{\n    Builder, BuilderConfig, BuilderConfigDiff, BuilderConfigVariant,\n    BuilderListItem, BuilderListItemInfo, BuilderQuerySpecifics,\n    PartialBuilderConfig, PartialServerBuilderConfig,\n  },\n  permission::{PermissionLevel, SpecificPermission},\n  resource::Resource,\n  server::Server,\n  update::Update,\n  user::User,\n};\n\nuse crate::state::db_client;\n\nimpl super::KomodoResource for Builder {\n  type Config = BuilderConfig;\n  type PartialConfig = PartialBuilderConfig;\n  type ConfigDiff = BuilderConfigDiff;\n  type Info = ();\n  type ListItem = BuilderListItem;\n  type QuerySpecifics = BuilderQuerySpecifics;\n\n  fn resource_type() -> ResourceTargetVariant {\n    ResourceTargetVariant::Builder\n  }\n\n  fn resource_target(id: impl Into<String>) -> ResourceTarget {\n    ResourceTarget::Builder(id.into())\n  }\n\n  fn creator_specific_permissions() -> IndexSet<SpecificPermission> {\n    [SpecificPermission::Attach].into_iter().collect()\n  }\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>\n  {\n    &db_client().builders\n  }\n\n  async fn to_list_item(\n    builder: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem {\n    let (builder_type, instance_type) = match builder.config {\n      BuilderConfig::Url(_) => {\n        (BuilderConfigVariant::Url.to_string(), None)\n      }\n      BuilderConfig::Server(config) => (\n        BuilderConfigVariant::Server.to_string(),\n        Some(config.server_id),\n      ),\n      BuilderConfig::Aws(config) => (\n        BuilderConfigVariant::Aws.to_string(),\n        Some(config.instance_type),\n      ),\n    };\n    BuilderListItem {\n      name: builder.name,\n      id: builder.id,\n      template: builder.template,\n      tags: builder.tags,\n      resource_type: ResourceTargetVariant::Builder,\n      info: BuilderListItemInfo {\n        builder_type,\n        instance_type,\n      },\n    }\n  }\n\n  async fn busy(_id: &String) -> anyhow::Result<bool> {\n    Ok(false)\n  }\n\n  // CREATE\n\n  fn create_operation() -> Operation {\n    Operation::CreateBuilder\n  }\n\n  fn user_can_create(user: &User) -> bool {\n    user.admin\n  }\n\n  async fn validate_create_config(\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_create(\n    _created: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  // UPDATE\n\n  fn update_operation() -> Operation {\n    Operation::UpdateBuilder\n  }\n\n  async fn validate_update_config(\n    _id: &str,\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  fn update_document(\n    original: Resource<Self::Config, Self::Info>,\n    config: Self::PartialConfig,\n  ) -> Result<Document, database::mungos::mongodb::bson::ser::Error>\n  {\n    let config = original.config.merge_partial(config);\n    to_document(&config)\n  }\n\n  async fn post_update(\n    _updated: &Self,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  // RENAME\n\n  fn rename_operation() -> Operation {\n    Operation::RenameBuilder\n  }\n\n  // DELETE\n\n  fn delete_operation() -> Operation {\n    Operation::DeleteBuilder\n  }\n\n  async fn pre_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    db_client()\n      .builds\n      .update_many(\n        doc! { \"config.builder_id\": &resource.id },\n        database::mungos::update::Update::Set(\n          doc! { \"config.builder_id\": \"\" },\n        ),\n      )\n      .await\n      .context(\"failed to update_many builds on database\")?;\n    db_client()\n      .repos\n      .update_many(\n        doc! { \"config.builder_id\": &resource.id },\n        database::mungos::update::Update::Set(\n          doc! { \"config.builder_id\": \"\" },\n        ),\n      )\n      .await\n      .context(\"failed to update_many repos on database\")?;\n    Ok(())\n  }\n\n  async fn post_delete(\n    _resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n}\n\n#[instrument(skip(user))]\nasync fn validate_config(\n  config: &mut PartialBuilderConfig,\n  user: &User,\n) -> anyhow::Result<()> {\n  match config {\n    PartialBuilderConfig::Server(PartialServerBuilderConfig {\n      server_id: Some(server_id),\n    }) if !server_id.is_empty() => {\n      let server = super::get_check_permissions::<Server>(\n        server_id,\n        user,\n        PermissionLevel::Read.attach(),\n      )\n      .await?;\n      *server_id = server.id;\n    }\n    _ => {}\n  }\n  Ok(())\n}\n"
  },
  {
    "path": "bin/core/src/resource/deployment.rs",
    "content": "use anyhow::Context;\nuse database::mungos::mongodb::Collection;\nuse formatting::format_serror;\nuse indexmap::IndexSet;\nuse komodo_client::entities::{\n  Operation, ResourceTarget, ResourceTargetVariant,\n  build::Build,\n  deployment::{\n    Deployment, DeploymentConfig, DeploymentConfigDiff,\n    DeploymentImage, DeploymentListItem, DeploymentListItemInfo,\n    DeploymentQuerySpecifics, DeploymentState,\n    PartialDeploymentConfig, conversions_from_str,\n  },\n  environment_vars_from_str,\n  permission::{PermissionLevel, SpecificPermission},\n  resource::Resource,\n  server::Server,\n  to_container_compatible_name,\n  update::Update,\n  user::User,\n};\nuse periphery_client::api::container::RemoveContainer;\n\nuse crate::{\n  config::core_config,\n  helpers::{\n    empty_or_only_spaces, periphery_client,\n    query::get_deployment_state,\n  },\n  monitor::update_cache_for_server,\n  state::{action_states, db_client, deployment_status_cache},\n};\n\nuse super::get_check_permissions;\n\nimpl super::KomodoResource for Deployment {\n  type Config = DeploymentConfig;\n  type PartialConfig = PartialDeploymentConfig;\n  type ConfigDiff = DeploymentConfigDiff;\n  type Info = ();\n  type ListItem = DeploymentListItem;\n  type QuerySpecifics = DeploymentQuerySpecifics;\n\n  fn resource_type() -> ResourceTargetVariant {\n    ResourceTargetVariant::Deployment\n  }\n\n  fn resource_target(id: impl Into<String>) -> ResourceTarget {\n    ResourceTarget::Deployment(id.into())\n  }\n\n  fn validated_name(name: &str) -> String {\n    to_container_compatible_name(name)\n  }\n\n  fn creator_specific_permissions() -> IndexSet<SpecificPermission> {\n    [\n      SpecificPermission::Inspect,\n      SpecificPermission::Logs,\n      SpecificPermission::Terminal,\n    ]\n    .into_iter()\n    .collect()\n  }\n\n  fn inherit_specific_permissions_from(\n    _self: &Resource<Self::Config, Self::Info>,\n  ) -> Option<ResourceTarget> {\n    ResourceTarget::Server(_self.config.server_id.clone()).into()\n  }\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>\n  {\n    &db_client().deployments\n  }\n\n  async fn to_list_item(\n    deployment: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem {\n    let status = deployment_status_cache().get(&deployment.id).await;\n    let state = if action_states()\n      .deployment\n      .get(&deployment.id)\n      .await\n      .map(|s| s.get().map(|s| s.deploying))\n      .transpose()\n      .ok()\n      .flatten()\n      .unwrap_or_default()\n    {\n      DeploymentState::Deploying\n    } else {\n      status.as_ref().map(|s| s.curr.state).unwrap_or_default()\n    };\n    let (build_image, build_id) = match deployment.config.image {\n      DeploymentImage::Build { build_id, version } => {\n        let (build_name, build_id, build_version) =\n          super::get::<Build>(&build_id)\n            .await\n            .map(|b| (b.name, b.id, b.config.version))\n            .unwrap_or((\n              String::from(\"unknown\"),\n              String::new(),\n              Default::default(),\n            ));\n        let version = if version.is_none() {\n          build_version.to_string()\n        } else {\n          version.to_string()\n        };\n        (format!(\"{build_name}:{version}\"), Some(build_id))\n      }\n      DeploymentImage::Image { image } => (image, None),\n    };\n    let (image, update_available) = status\n      .as_ref()\n      .and_then(|s| {\n        s.curr.container.as_ref().map(|c| {\n          (\n            c.image\n              .clone()\n              .unwrap_or_else(|| String::from(\"Unknown\")),\n            s.curr.update_available,\n          )\n        })\n      })\n      .unwrap_or((build_image, false));\n    DeploymentListItem {\n      name: deployment.name,\n      id: deployment.id,\n      template: deployment.template,\n      tags: deployment.tags,\n      resource_type: ResourceTargetVariant::Deployment,\n      info: DeploymentListItemInfo {\n        state,\n        status: status.as_ref().and_then(|s| {\n          s.curr.container.as_ref().and_then(|c| c.status.to_owned())\n        }),\n        image,\n        update_available,\n        server_id: deployment.config.server_id,\n        build_id,\n      },\n    }\n  }\n\n  async fn busy(id: &String) -> anyhow::Result<bool> {\n    action_states()\n      .deployment\n      .get(id)\n      .await\n      .unwrap_or_default()\n      .busy()\n  }\n\n  // CREATE\n\n  fn create_operation() -> Operation {\n    Operation::CreateDeployment\n  }\n\n  fn user_can_create(user: &User) -> bool {\n    user.admin || !core_config().disable_non_admin_create\n  }\n\n  async fn validate_create_config(\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_create(\n    created: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    if created.config.server_id.is_empty() {\n      return Ok(());\n    }\n    let Ok(server) = super::get::<Server>(&created.config.server_id)\n      .await\n      .inspect_err(|e| {\n        warn!(\n          \"Failed to get Server for Deployment {} | {e:#}\",\n          created.name\n        )\n      })\n    else {\n      return Ok(());\n    };\n    update_cache_for_server(&server, true).await;\n    Ok(())\n  }\n\n  // UPDATE\n\n  fn update_operation() -> Operation {\n    Operation::UpdateDeployment\n  }\n\n  async fn validate_update_config(\n    _id: &str,\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_update(\n    updated: &Self,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Self::post_create(updated, update).await\n  }\n\n  // RENAME\n\n  fn rename_operation() -> Operation {\n    Operation::RenameDeployment\n  }\n\n  // DELETE\n\n  fn delete_operation() -> Operation {\n    Operation::DeleteDeployment\n  }\n\n  async fn pre_delete(\n    deployment: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    let state = get_deployment_state(&deployment.id)\n      .await\n      .context(\"Failed to get deployment state\")?;\n    if matches!(\n      state,\n      DeploymentState::NotDeployed | DeploymentState::Unknown\n    ) {\n      return Ok(());\n    }\n    // container needs to be destroyed\n    let server = match super::get::<Server>(\n      &deployment.config.server_id,\n    )\n    .await\n    {\n      Ok(server) => server,\n      Err(e) => {\n        update.push_error_log(\n          \"Remove Container\",\n          format_serror(\n            &e.context(format!(\n              \"failed to retrieve server at {} from db.\",\n              deployment.config.server_id\n            ))\n            .into(),\n          ),\n        );\n        return Ok(());\n      }\n    };\n    if !server.config.enabled {\n      // Don't need to\n      update.push_simple_log(\n        \"Remove Container\",\n        \"Skipping container removal, server is disabled.\",\n      );\n      return Ok(());\n    }\n    let periphery = match periphery_client(&server) {\n      Ok(periphery) => periphery,\n      Err(e) => {\n        // This case won't ever happen, as periphery_client only fallible if the server is disabled.\n        // Leaving it for completeness sake\n        update.push_error_log(\n          \"Remove Container\",\n          format_serror(\n            &e.context(\"Failed to get periphery client\").into(),\n          ),\n        );\n        return Ok(());\n      }\n    };\n    match periphery\n      .request(RemoveContainer {\n        name: deployment.name.clone(),\n        signal: deployment.config.termination_signal.into(),\n        time: deployment.config.termination_timeout.into(),\n      })\n      .await\n    {\n      Ok(log) => update.logs.push(log),\n      Err(e) => update.push_error_log(\n        \"Remove Container\",\n        format_serror(\n          &e.context(\"Failed to remove container\").into(),\n        ),\n      ),\n    };\n    Ok(())\n  }\n\n  async fn post_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    deployment_status_cache().remove(&resource.id).await;\n    Ok(())\n  }\n}\n\n#[instrument(skip(user))]\nasync fn validate_config(\n  config: &mut PartialDeploymentConfig,\n  user: &User,\n) -> anyhow::Result<()> {\n  if let Some(server_id) = &config.server_id\n    && !server_id.is_empty()\n  {\n    let server = get_check_permissions::<Server>(\n      server_id,\n      user,\n      PermissionLevel::Read.attach(),\n    )\n    .await\n    .context(\"Cannot attach Deployment to this Server\")?;\n    config.server_id = Some(server.id);\n  }\n  if let Some(DeploymentImage::Build { build_id, version }) =\n    &config.image\n    && !build_id.is_empty()\n  {\n    let build = get_check_permissions::<Build>(\n      build_id,\n      user,\n      PermissionLevel::Read.attach(),\n    )\n    .await\n    .context(\"Cannot update deployment with this build attached.\")?;\n    config.image = Some(DeploymentImage::Build {\n      build_id: build.id,\n      version: *version,\n    });\n  }\n  if let Some(volumes) = &config.volumes {\n    conversions_from_str(volumes).context(\"Invalid volumes\")?;\n  }\n  if let Some(ports) = &config.ports {\n    conversions_from_str(ports).context(\"Invalid ports\")?;\n  }\n  if let Some(environment) = &config.environment {\n    environment_vars_from_str(environment)\n      .context(\"Invalid environment\")?;\n  }\n  if let Some(extra_args) = &mut config.extra_args {\n    extra_args.retain(|v| !empty_or_only_spaces(v))\n  }\n  Ok(())\n}\n"
  },
  {
    "path": "bin/core/src/resource/mod.rs",
    "content": "use std::{\n  collections::{HashMap, HashSet},\n  str::FromStr,\n};\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  by_id::{delete_one_by_id, update_one_by_id},\n  find::find_collect,\n  mongodb::{\n    Collection,\n    bson::{Document, doc, oid::ObjectId, to_document},\n    options::FindOptions,\n  },\n};\nuse formatting::format_serror;\nuse futures::future::join_all;\nuse indexmap::IndexSet;\nuse komodo_client::{\n  api::{read::ExportResourcesToToml, write::CreateTag},\n  entities::{\n    Operation, ResourceTarget, ResourceTargetVariant,\n    komodo_timestamp,\n    permission::{\n      PermissionLevel, PermissionLevelAndSpecifics,\n      SpecificPermission,\n    },\n    resource::{AddFilters, Resource, ResourceQuery},\n    tag::Tag,\n    to_general_name,\n    update::Update,\n    user::{User, system_user},\n  },\n  parsers::parse_string_list,\n};\nuse partial_derive2::{Diff, MaybeNone, PartialDiff};\nuse reqwest::StatusCode;\nuse resolver_api::Resolve;\nuse serde::{Serialize, de::DeserializeOwned};\nuse serror::AddStatusCodeError;\n\nuse crate::{\n  api::{read::ReadArgs, write::WriteArgs},\n  helpers::{\n    create_permission, flatten_document,\n    query::{get_tag, id_or_name_filter},\n    update::{add_update, make_update},\n  },\n  permission::{get_check_permissions, get_resource_ids_for_user},\n  state::db_client,\n};\n\nmod action;\nmod alerter;\nmod build;\nmod builder;\nmod deployment;\nmod procedure;\nmod refresh;\nmod repo;\nmod server;\nmod stack;\nmod sync;\n\npub use action::{\n  refresh_action_state_cache, spawn_action_state_refresh_loop,\n};\npub use build::{\n  refresh_build_state_cache, spawn_build_state_refresh_loop,\n};\npub use procedure::{\n  refresh_procedure_state_cache, spawn_procedure_state_refresh_loop,\n};\npub use refresh::{\n  refresh_all_resources_cache,\n  spawn_all_resources_cache_refresh_loop,\n  spawn_resource_refresh_loop,\n};\npub use repo::{\n  refresh_repo_state_cache, spawn_repo_state_refresh_loop,\n};\n\n/// Implement on each Komodo resource for common methods\npub trait KomodoResource {\n  type ListItem: Serialize + Send;\n  type Config: Clone\n    + Default\n    + Send\n    + Sync\n    + Unpin\n    + Serialize\n    + DeserializeOwned\n    + From<Self::PartialConfig>\n    + PartialDiff<Self::PartialConfig, Self::ConfigDiff>\n    + 'static;\n  type PartialConfig: Clone\n    + Default\n    + From<Self::Config>\n    + Serialize\n    + MaybeNone;\n  type ConfigDiff: Into<Self::PartialConfig>\n    + Serialize\n    + Diff\n    + MaybeNone;\n  type Info: Clone\n    + Send\n    + Sync\n    + Unpin\n    + Default\n    + Serialize\n    + DeserializeOwned\n    + 'static;\n  type QuerySpecifics: AddFilters + Default + std::fmt::Debug;\n\n  fn resource_type() -> ResourceTargetVariant;\n  fn resource_target(id: impl Into<String>) -> ResourceTarget;\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>;\n\n  async fn to_list_item(\n    resource: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem;\n\n  #[allow(clippy::ptr_arg)]\n  async fn busy(id: &String) -> anyhow::Result<bool>;\n\n  /// Some resource types have restrictions on the allowed formatting for names.\n  /// Stacks, Builds, and Deployments all require names to be \"docker compatible\",\n  /// which means all lowercase, and no spaces or dots.\n  fn validated_name(name: &str) -> String {\n    to_general_name(name)\n  }\n\n  /// These permissions go to the creator of the resource,\n  /// and include full access to the resource.\n  fn creator_specific_permissions() -> IndexSet<SpecificPermission> {\n    IndexSet::new()\n  }\n\n  /// For Stacks / Deployments, they should inherit specific\n  /// permissions like `Logs`, `Inspect`, and `Terminal`\n  /// from their attached Server.\n  fn inherit_specific_permissions_from(\n    _self: &Resource<Self::Config, Self::Info>,\n  ) -> Option<ResourceTarget> {\n    None\n  }\n\n  // =======\n  // CREATE\n  // =======\n\n  fn create_operation() -> Operation;\n\n  fn user_can_create(user: &User) -> bool;\n\n  async fn validate_create_config(\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()>;\n\n  async fn default_info() -> anyhow::Result<Self::Info> {\n    Ok(Default::default())\n  }\n\n  async fn post_create(\n    created: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()>;\n\n  // =======\n  // UPDATE\n  // =======\n\n  fn update_operation() -> Operation;\n\n  async fn validate_update_config(\n    id: &str,\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()>;\n\n  /// Should be overridden for enum configs, eg Alerter, Builder, ...\n  fn update_document(\n    _original: Resource<Self::Config, Self::Info>,\n    config: Self::PartialConfig,\n  ) -> Result<Document, database::mungos::mongodb::bson::ser::Error>\n  {\n    to_document(&config)\n  }\n\n  /// Run any required task after resource updated in database but\n  /// before the request resolves.\n  async fn post_update(\n    updated: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()>;\n\n  // =======\n  // RENAME\n  // =======\n\n  fn rename_operation() -> Operation;\n\n  // =======\n  // DELETE\n  // =======\n\n  fn delete_operation() -> Operation;\n\n  /// Clean up all links to this resource before deleting it.\n  async fn pre_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()>;\n\n  /// Run any required task after resource deleted from database but\n  /// before the request resolves.\n  async fn post_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()>;\n}\n\n// Methods\n\n// ======\n// GET\n// ======\n\npub async fn get<T: KomodoResource>(\n  id_or_name: &str,\n) -> anyhow::Result<Resource<T::Config, T::Info>> {\n  if id_or_name.is_empty() {\n    return Err(anyhow!(\n      \"Cannot find {} with empty name / id\",\n      T::resource_type()\n    ));\n  }\n  T::coll()\n    .find_one(id_or_name_filter(id_or_name))\n    .await\n    .context(\"failed to query db for resource\")?\n    .with_context(|| {\n      format!(\n        \"did not find any {} matching {id_or_name}\",\n        T::resource_type()\n      )\n    })\n}\n\n// ======\n// LIST\n// ======\n\n/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access).\n#[instrument(level = \"debug\")]\npub async fn get_resource_object_ids_for_user<T: KomodoResource>(\n  user: &User,\n) -> anyhow::Result<Option<Vec<ObjectId>>> {\n  get_resource_ids_for_user::<T>(user).await.map(|ids| {\n    ids.map(|ids| {\n      ids\n        .into_iter()\n        .flat_map(|id| ObjectId::from_str(&id))\n        .collect()\n    })\n  })\n}\n\n#[instrument(level = \"debug\")]\npub async fn list_for_user<T: KomodoResource>(\n  mut query: ResourceQuery<T::QuerySpecifics>,\n  user: &User,\n  permissions: PermissionLevelAndSpecifics,\n  all_tags: &[Tag],\n) -> anyhow::Result<Vec<T::ListItem>> {\n  validate_resource_query_tags(&mut query, all_tags)?;\n  let mut filters = Document::new();\n  query.add_filters(&mut filters);\n  list_for_user_using_document::<T>(filters, user, permissions).await\n}\n\n// #[instrument(level = \"debug\")]\n// pub async fn list_for_user_using_pattern<T: KomodoResource>(\n//   pattern: &str,\n//   query: ResourceQuery<T::QuerySpecifics>,\n//   user: &User,\n//   permissions: PermissionLevelAndSpecifics,\n//   all_tags: &[Tag],\n// ) -> anyhow::Result<Vec<T::ListItem>> {\n//   let list = list_full_for_user_using_pattern::<T>(\n//     pattern,\n//     query,\n//     user,\n//     permissions,\n//     all_tags,\n//   )\n//   .await?\n//   .into_iter()\n//   .map(|resource| T::to_list_item(resource));\n//   Ok(join_all(list).await)\n// }\n\n#[instrument(level = \"debug\")]\npub async fn list_for_user_using_document<T: KomodoResource>(\n  filters: Document,\n  user: &User,\n  permissions: PermissionLevelAndSpecifics,\n) -> anyhow::Result<Vec<T::ListItem>> {\n  let list = list_full_for_user_using_document::<T>(filters, user)\n    .await?\n    .into_iter()\n    .map(|resource| T::to_list_item(resource));\n  Ok(join_all(list).await)\n}\n\n/// Lists full resource matching wildcard syntax,\n/// or regex if wrapped with \"\\\\\"\n///\n/// ## Example\n/// ```\n/// let items = list_full_for_user_using_match_string::<Build>(\"foo-*\", Default::default(), user, all_tags).await?;\n/// let items = list_full_for_user_using_match_string::<Build>(\"\\\\^foo-.*$\\\\\", Default::default(), user, all_tags).await?;\n/// ```\n#[instrument(level = \"debug\")]\npub async fn list_full_for_user_using_pattern<T: KomodoResource>(\n  pattern: &str,\n  query: ResourceQuery<T::QuerySpecifics>,\n  user: &User,\n  permissions: PermissionLevelAndSpecifics,\n  all_tags: &[Tag],\n) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {\n  let resources =\n    list_full_for_user::<T>(query, user, permissions, all_tags)\n      .await?;\n\n  let patterns = parse_string_list(pattern);\n  let mut names = HashSet::<String>::new();\n\n  for pattern in patterns {\n    if pattern.starts_with('\\\\') && pattern.ends_with('\\\\') {\n      let regex = regex::Regex::new(&pattern[1..(pattern.len() - 1)])\n        .context(\"Regex matching string invalid\")?;\n      for resource in &resources {\n        if regex.is_match(&resource.name) {\n          names.insert(resource.name.clone());\n        }\n      }\n    } else {\n      let wildcard = wildcard::Wildcard::new(pattern.as_bytes())\n        .context(\"Wildcard matching string invalid\")?;\n      for resource in &resources {\n        if wildcard.is_match(resource.name.as_bytes()) {\n          names.insert(resource.name.clone());\n        }\n      }\n    };\n  }\n\n  Ok(\n    resources\n      .into_iter()\n      .filter(|resource| names.contains(resource.name.as_str()))\n      .collect(),\n  )\n}\n\n#[instrument(level = \"debug\")]\npub async fn list_full_for_user<T: KomodoResource>(\n  mut query: ResourceQuery<T::QuerySpecifics>,\n  user: &User,\n  permissions: PermissionLevelAndSpecifics,\n  all_tags: &[Tag],\n) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {\n  validate_resource_query_tags(&mut query, all_tags)?;\n  let mut filters = Document::new();\n  query.add_filters(&mut filters);\n  list_full_for_user_using_document::<T>(filters, user).await\n}\n\n#[instrument(level = \"debug\")]\npub async fn list_full_for_user_using_document<T: KomodoResource>(\n  mut filters: Document,\n  user: &User,\n) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {\n  if let Some(ids) =\n    get_resource_object_ids_for_user::<T>(user).await?\n  {\n    filters.insert(\"_id\", doc! { \"$in\": ids });\n  }\n  find_collect(\n    T::coll(),\n    filters,\n    FindOptions::builder().sort(doc! { \"name\": 1 }).build(),\n  )\n  .await\n  .with_context(|| {\n    format!(\"failed to pull {}s from mongo\", T::resource_type())\n  })\n}\n\npub type IdResourceMap<T> = HashMap<\n  String,\n  Resource<\n    <T as KomodoResource>::Config,\n    <T as KomodoResource>::Info,\n  >,\n>;\n\n#[instrument(level = \"debug\")]\npub async fn get_id_to_resource_map<T: KomodoResource>(\n  id_to_tags: &HashMap<String, Tag>,\n  match_tags: &[String],\n) -> anyhow::Result<IdResourceMap<T>> {\n  let res = find_collect(T::coll(), None, None)\n    .await\n    .with_context(|| {\n      format!(\"failed to pull {}s from mongo\", T::resource_type())\n    })?\n    .into_iter()\n    .filter(|resource| {\n      if match_tags.is_empty() {\n        return true;\n      }\n      for tag in match_tags.iter() {\n        for resource_tag in &resource.tags {\n          match ObjectId::from_str(resource_tag) {\n            Ok(_) => match id_to_tags\n              .get(resource_tag)\n              .map(|tag| tag.name.as_str())\n            {\n              Some(name) => {\n                if tag != name {\n                  return false;\n                }\n              }\n              None => return false,\n            },\n            Err(_) => {\n              if resource_tag != tag {\n                return false;\n              }\n            }\n          }\n        }\n      }\n      true\n    })\n    .map(|r| (r.id.clone(), r))\n    .collect();\n  Ok(res)\n}\n\n// =======\n// CREATE\n// =======\n\npub async fn create<T: KomodoResource>(\n  name: &str,\n  mut config: T::PartialConfig,\n  user: &User,\n) -> serror::Result<Resource<T::Config, T::Info>> {\n  if !T::user_can_create(user) {\n    return Err(\n      anyhow!(\n        \"User does not have permissions to create {}.\",\n        T::resource_type()\n      )\n      .status_code(StatusCode::FORBIDDEN),\n    );\n  }\n\n  if name.is_empty() {\n    return Err(\n      anyhow!(\"Must provide non-empty name for resource\")\n        .status_code(StatusCode::BAD_REQUEST),\n    );\n  }\n\n  let name = T::validated_name(name);\n\n  if ObjectId::from_str(&name).is_ok() {\n    return Err(\n      anyhow!(\"Valid ObjectIds cannot be used as names\")\n        .status_code(StatusCode::BAD_REQUEST),\n    );\n  }\n\n  // Ensure an existing resource with same name doesn't already exist\n  // The database indexing also ensures this but doesn't give a good error message.\n  if list_full_for_user::<T>(\n    Default::default(),\n    system_user(),\n    PermissionLevel::Read.into(),\n    &[],\n  )\n  .await\n  .context(\"Failed to list all resources for duplicate name check\")?\n  .into_iter()\n  .any(|r| r.name == name)\n  {\n    return Err(\n      anyhow!(\"Resource with name '{}' already exists\", name)\n        .status_code(StatusCode::CONFLICT),\n    );\n  }\n\n  let start_ts = komodo_timestamp();\n\n  T::validate_create_config(&mut config, user).await?;\n\n  let resource = Resource::<T::Config, T::Info> {\n    id: Default::default(),\n    name,\n    description: Default::default(),\n    template: Default::default(),\n    tags: Default::default(),\n    config: config.into(),\n    info: T::default_info().await?,\n    base_permission: PermissionLevel::None.into(),\n    updated_at: start_ts,\n  };\n\n  let resource_id = T::coll()\n    .insert_one(&resource)\n    .await\n    .with_context(|| {\n      format!(\"failed to add {} to db\", T::resource_type())\n    })?\n    .inserted_id\n    .as_object_id()\n    .context(\"inserted_id is not ObjectId\")?\n    .to_string();\n\n  let resource = get::<T>(&resource_id).await?;\n  let target = resource_target::<T>(resource_id);\n\n  create_permission(\n    user,\n    target.clone(),\n    PermissionLevel::Write,\n    T::creator_specific_permissions(),\n  )\n  .await;\n\n  let mut update = make_update(target, T::create_operation(), user);\n  update.start_ts = start_ts;\n  update.push_simple_log(\n    &format!(\"create {}\", T::resource_type()),\n    format!(\n      \"created {}\\nid: {}\\nname: {}\",\n      T::resource_type(),\n      resource.id,\n      resource.name\n    ),\n  );\n  update.push_simple_log(\n    \"config\",\n    serde_json::to_string_pretty(&resource.config)\n      .context(\"failed to serialize resource config to JSON\")?,\n  );\n\n  T::post_create(&resource, &mut update).await?;\n\n  refresh_all_resources_cache().await;\n\n  update.finalize();\n  add_update(update).await?;\n\n  Ok(resource)\n}\n\n// =======\n// UPDATE\n// =======\n\npub async fn update<T: KomodoResource>(\n  id_or_name: &str,\n  mut config: T::PartialConfig,\n  user: &User,\n) -> anyhow::Result<Resource<T::Config, T::Info>> {\n  let resource = get_check_permissions::<T>(\n    id_or_name,\n    user,\n    PermissionLevel::Write.into(),\n  )\n  .await?;\n\n  if T::busy(&resource.id).await? {\n    return Err(anyhow!(\"{} busy\", T::resource_type()));\n  }\n\n  T::validate_update_config(&resource.id, &mut config, user).await?;\n\n  // Gets a diff object.\n  let diff = resource.config.partial_diff(config);\n\n  if diff.is_none() {\n    return Ok(resource);\n  }\n\n  // Leave this Result unhandled for now\n  let prev_toml = ExportResourcesToToml {\n    targets: vec![T::resource_target(&resource.id)],\n    ..Default::default()\n  }\n  .resolve(&ReadArgs {\n    user: system_user().to_owned(),\n  })\n  .await\n  .map_err(|e| e.error)\n  .context(\"Failed to export resource toml before update\");\n\n  // This minimizes the update against the existing config\n  let config: T::PartialConfig = diff.into();\n\n  let id = resource.id.clone();\n\n  let config_doc = T::update_document(resource, config)\n    .context(\"failed to serialize config to bson document\")?;\n\n  let update_doc = flatten_document(doc! { \"config\": config_doc });\n\n  update_one_by_id(T::coll(), &id, doc! { \"$set\": update_doc }, None)\n    .await\n    .context(\"failed to update resource on database\")?;\n\n  let curr_toml = ExportResourcesToToml {\n    targets: vec![T::resource_target(&id)],\n    ..Default::default()\n  }\n  .resolve(&ReadArgs {\n    user: system_user().to_owned(),\n  })\n  .await\n  .map_err(|e| e.error)\n  .context(\"Failed to export resource toml after update\");\n\n  let mut update = make_update(\n    resource_target::<T>(id),\n    T::update_operation(),\n    user,\n  );\n\n  match prev_toml {\n    Ok(res) => update.prev_toml = res.toml,\n    Err(e) => update\n      // These logs are pushed with success == true, so user still knows the update was succesful.\n      .push_simple_log(\"Failed export\", format_serror(&e.into())),\n  }\n  match curr_toml {\n    Ok(res) => update.current_toml = res.toml,\n    Err(e) => update\n      // These logs are pushed with success == true, so user still knows the update was succesful.\n      .push_simple_log(\"Failed export\", format_serror(&e.into())),\n  }\n\n  let updated = get::<T>(id_or_name).await?;\n\n  T::post_update(&updated, &mut update).await?;\n\n  refresh_all_resources_cache().await;\n\n  update.finalize();\n  add_update(update).await?;\n\n  Ok(updated)\n}\n\nfn resource_target<T: KomodoResource>(id: String) -> ResourceTarget {\n  match T::resource_type() {\n    ResourceTargetVariant::System => ResourceTarget::System(id),\n    ResourceTargetVariant::Build => ResourceTarget::Build(id),\n    ResourceTargetVariant::Builder => ResourceTarget::Builder(id),\n    ResourceTargetVariant::Deployment => {\n      ResourceTarget::Deployment(id)\n    }\n    ResourceTargetVariant::Server => ResourceTarget::Server(id),\n    ResourceTargetVariant::Repo => ResourceTarget::Repo(id),\n    ResourceTargetVariant::Alerter => ResourceTarget::Alerter(id),\n    ResourceTargetVariant::Procedure => ResourceTarget::Procedure(id),\n    ResourceTargetVariant::ResourceSync => {\n      ResourceTarget::ResourceSync(id)\n    }\n    ResourceTargetVariant::Stack => ResourceTarget::Stack(id),\n    ResourceTargetVariant::Action => ResourceTarget::Action(id),\n  }\n}\n\npub struct ResourceMetaUpdate {\n  pub description: Option<String>,\n  pub template: Option<bool>,\n  pub tags: Option<Vec<String>>,\n}\n\nimpl ResourceMetaUpdate {\n  pub fn is_none(&self) -> bool {\n    self.description.is_none()\n      && self.template.is_none()\n      && self.tags.is_none()\n  }\n}\n\npub async fn update_meta<T: KomodoResource>(\n  id_or_name: &str,\n  meta: ResourceMetaUpdate,\n  args: &WriteArgs,\n) -> anyhow::Result<()> {\n  get_check_permissions::<T>(\n    id_or_name,\n    &args.user,\n    PermissionLevel::Write.into(),\n  )\n  .await?;\n  let mut set = Document::new();\n  if let Some(description) = meta.description {\n    set.insert(\"description\", description);\n  }\n  if let Some(template) = meta.template {\n    set.insert(\"template\", template);\n  }\n  if let Some(tags) = meta.tags {\n    // First normalize to tag ids only\n    let futures = tags.iter().map(|tag| async {\n      match get_tag(tag).await {\n        Ok(tag) => Ok(tag.id),\n        Err(_) => CreateTag {\n          name: tag.to_string(),\n          color: None,\n        }\n        .resolve(args)\n        .await\n        .map(|tag| tag.id),\n      }\n    });\n    let tags = join_all(futures)\n      .await\n      .into_iter()\n      .flatten()\n      .collect::<Vec<_>>();\n    set.insert(\"tags\", tags);\n  }\n  T::coll()\n    .update_one(id_or_name_filter(id_or_name), doc! { \"$set\": set })\n    .await?;\n  refresh_all_resources_cache().await;\n  Ok(())\n}\n\npub async fn remove_tag_from_all<T: KomodoResource>(\n  tag_id: &str,\n) -> anyhow::Result<()> {\n  T::coll()\n    .update_many(doc! {}, doc! { \"$pull\": { \"tags\": tag_id } })\n    .await\n    .context(\"failed to remove tag from resources\")?;\n  Ok(())\n}\n\n// =======\n// RENAME\n// =======\n\npub async fn rename<T: KomodoResource>(\n  id_or_name: &str,\n  name: &str,\n  user: &User,\n) -> anyhow::Result<Update> {\n  let resource = get_check_permissions::<T>(\n    id_or_name,\n    user,\n    PermissionLevel::Write.into(),\n  )\n  .await?;\n\n  let mut update = make_update(\n    resource_target::<T>(resource.id.clone()),\n    T::rename_operation(),\n    user,\n  );\n\n  let name = T::validated_name(name);\n\n  update_one_by_id(\n    T::coll(),\n    &resource.id,\n    database::mungos::update::Update::Set(\n      doc! { \"name\": &name, \"updated_at\": komodo_timestamp() },\n    ),\n    None,\n  )\n  .await\n  .with_context(|| {\n    format!(\n      \"Failed to update {ty} on db. This name may already be taken.\",\n      ty = T::resource_type()\n    )\n  })?;\n\n  update.push_simple_log(\n    &format!(\"Rename {}\", T::resource_type()),\n    format!(\n      \"Renamed {ty} {id} from {prev_name} to {name}\",\n      ty = T::resource_type(),\n      id = resource.id,\n      prev_name = resource.name\n    ),\n  );\n\n  refresh_all_resources_cache().await;\n\n  update.finalize();\n  update.id = add_update(update.clone()).await?;\n\n  Ok(update)\n}\n\n// =======\n// DELETE\n// =======\n\npub async fn delete<T: KomodoResource>(\n  id_or_name: &str,\n  args: &WriteArgs,\n) -> anyhow::Result<Resource<T::Config, T::Info>> {\n  let resource = get_check_permissions::<T>(\n    id_or_name,\n    &args.user,\n    PermissionLevel::Write.into(),\n  )\n  .await?;\n\n  if T::busy(&resource.id).await? {\n    return Err(anyhow!(\"{} busy\", T::resource_type()));\n  }\n\n  let target = resource_target::<T>(resource.id.clone());\n  let toml = ExportResourcesToToml {\n    targets: vec![target.clone()],\n    ..Default::default()\n  }\n  .resolve(&ReadArgs {\n    user: args.user.clone(),\n  })\n  .await\n  .map_err(|e| e.error)?\n  .toml;\n\n  let mut update =\n    make_update(target.clone(), T::delete_operation(), &args.user);\n\n  T::pre_delete(&resource, &mut update).await?;\n\n  delete_all_permissions_on_resource(target.clone()).await;\n  remove_from_recently_viewed(target.clone()).await;\n\n  delete_one_by_id(T::coll(), &resource.id, None)\n    .await\n    .with_context(|| {\n      format!(\"Failed to delete {} from database\", T::resource_type())\n    })?;\n\n  update.push_simple_log(\n    &format!(\"Delete {}\", T::resource_type()),\n    format!(\"Deleted {} {}\", T::resource_type(), resource.name),\n  );\n  update.push_simple_log(\"Deleted Toml\", toml);\n\n  tokio::join!(\n    async {\n      if let Err(e) = T::post_delete(&resource, &mut update).await {\n        update\n          .push_error_log(\"post delete\", format_serror(&e.into()));\n      }\n    },\n    delete_from_alerters::<T>(&resource.id)\n  );\n\n  refresh_all_resources_cache().await;\n\n  update.finalize();\n  add_update(update).await?;\n\n  Ok(resource)\n}\n\nasync fn delete_from_alerters<T: KomodoResource>(id: &str) {\n  let target_bson = doc! {\n    \"type\": T::resource_type().as_ref(),\n    \"id\": id,\n  };\n  if let Err(e) = db_client()\n    .alerters\n    .update_many(Document::new(), doc! {\n      \"$pull\": {\n        \"config.resources\": &target_bson,\n        \"config.except_resources\": target_bson,\n      }\n    })\n    .await\n    .context(\"Failed to clear deleted resource from alerter whitelist / blacklist\")\n  {\n    warn!(\"{e:#}\");\n  }\n}\n\n// =======\n\n#[instrument(level = \"debug\")]\npub fn validate_resource_query_tags<T: Default + std::fmt::Debug>(\n  query: &mut ResourceQuery<T>,\n  all_tags: &[Tag],\n) -> anyhow::Result<()> {\n  query.tags = query\n    .tags\n    .iter()\n    .map(|tag| {\n      all_tags\n        .iter()\n        .find(|t| t.name == *tag || t.id == *tag)\n        .map(|tag| tag.id.clone())\n        .with_context(|| {\n          format!(\"No tag found matching name or id: {tag}\")\n        })\n    })\n    .collect::<anyhow::Result<Vec<_>>>()?;\n  Ok(())\n}\n\n#[instrument]\npub async fn delete_all_permissions_on_resource<T>(target: T)\nwhere\n  T: Into<ResourceTarget> + std::fmt::Debug,\n{\n  let target: ResourceTarget = target.into();\n  let (variant, id) = target.extract_variant_id();\n  if let Err(e) = db_client()\n    .permissions\n    .delete_many(doc! {\n      \"resource_target.type\": variant.as_ref(),\n      \"resource_target.id\": &id\n    })\n    .await\n  {\n    warn!(\n      \"failed to delete_many permissions matching target {target:?} | {e:#}\"\n    );\n  }\n}\n\n#[instrument]\npub async fn remove_from_recently_viewed<T>(resource: T)\nwhere\n  T: Into<ResourceTarget> + std::fmt::Debug,\n{\n  let resource: ResourceTarget = resource.into();\n  let (recent_field, id) = match resource {\n    ResourceTarget::Server(id) => (\"recents.Server\", id),\n    ResourceTarget::Deployment(id) => (\"recents.Deployment\", id),\n    ResourceTarget::Build(id) => (\"recents.Build\", id),\n    ResourceTarget::Repo(id) => (\"recents.Repo\", id),\n    ResourceTarget::Procedure(id) => (\"recents.Procedure\", id),\n    ResourceTarget::Action(id) => (\"recents.Action\", id),\n    ResourceTarget::Stack(id) => (\"recents.Stack\", id),\n    ResourceTarget::Builder(id) => (\"recents.Builder\", id),\n    ResourceTarget::Alerter(id) => (\"recents.Alerter\", id),\n    ResourceTarget::ResourceSync(id) => (\"recents.ResourceSync\", id),\n    ResourceTarget::System(_) => return,\n  };\n  if let Err(e) = db_client()\n    .users\n    .update_many(\n      doc! {},\n      doc! {\n        \"$pull\": {\n          recent_field: id\n        }\n      },\n    )\n    .await\n    .context(\"failed to remove resource from users recently viewed\")\n  {\n    warn!(\"{e:#}\");\n  }\n}\n"
  },
  {
    "path": "bin/core/src/resource/procedure.rs",
    "content": "use std::time::Duration;\n\nuse anyhow::{Context, anyhow};\nuse database::mungos::{\n  find::find_collect,\n  mongodb::{Collection, bson::doc, options::FindOneOptions},\n};\nuse futures::{TryStreamExt, stream::FuturesUnordered};\nuse komodo_client::{\n  api::execute::Execution,\n  entities::{\n    Operation, ResourceTarget, ResourceTargetVariant,\n    action::Action,\n    alerter::Alerter,\n    build::Build,\n    deployment::Deployment,\n    permission::PermissionLevel,\n    procedure::{\n      PartialProcedureConfig, Procedure, ProcedureConfig,\n      ProcedureConfigDiff, ProcedureListItem, ProcedureListItemInfo,\n      ProcedureQuerySpecifics, ProcedureState,\n    },\n    repo::Repo,\n    resource::Resource,\n    server::Server,\n    stack::Stack,\n    sync::ResourceSync,\n    update::Update,\n    user::User,\n  },\n};\n\nuse crate::{\n  config::core_config,\n  helpers::query::{get_last_run_at, get_procedure_state},\n  schedule::{\n    cancel_schedule, get_schedule_item_info, update_schedule,\n  },\n  state::{action_states, db_client, procedure_state_cache},\n};\n\nimpl super::KomodoResource for Procedure {\n  type Config = ProcedureConfig;\n  type PartialConfig = PartialProcedureConfig;\n  type ConfigDiff = ProcedureConfigDiff;\n  type Info = ();\n  type ListItem = ProcedureListItem;\n  type QuerySpecifics = ProcedureQuerySpecifics;\n\n  fn resource_type() -> ResourceTargetVariant {\n    ResourceTargetVariant::Procedure\n  }\n\n  fn resource_target(id: impl Into<String>) -> ResourceTarget {\n    ResourceTarget::Procedure(id.into())\n  }\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>\n  {\n    &db_client().procedures\n  }\n\n  async fn to_list_item(\n    procedure: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem {\n    let (state, last_run_at) = tokio::join!(\n      get_procedure_state(&procedure.id),\n      get_last_run_at::<Procedure>(&procedure.id)\n    );\n    let (next_scheduled_run, schedule_error) = get_schedule_item_info(\n      &ResourceTarget::Procedure(procedure.id.clone()),\n    );\n    ProcedureListItem {\n      name: procedure.name,\n      id: procedure.id,\n      template: procedure.template,\n      tags: procedure.tags,\n      resource_type: ResourceTargetVariant::Procedure,\n      info: ProcedureListItemInfo {\n        stages: procedure.config.stages.len() as i64,\n        state,\n        last_run_at: last_run_at.unwrap_or(None),\n        next_scheduled_run,\n        schedule_error,\n      },\n    }\n  }\n\n  async fn busy(id: &String) -> anyhow::Result<bool> {\n    action_states()\n      .procedure\n      .get(id)\n      .await\n      .unwrap_or_default()\n      .busy()\n  }\n\n  // CREATE\n\n  fn create_operation() -> Operation {\n    Operation::CreateProcedure\n  }\n\n  fn user_can_create(user: &User) -> bool {\n    user.admin || !core_config().disable_non_admin_create\n  }\n\n  async fn validate_create_config(\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user, None).await\n  }\n\n  async fn post_create(\n    created: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    update_schedule(created);\n    refresh_procedure_state_cache().await;\n    Ok(())\n  }\n\n  // UPDATE\n\n  fn update_operation() -> Operation {\n    Operation::UpdateProcedure\n  }\n\n  async fn validate_update_config(\n    id: &str,\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user, Some(id)).await\n  }\n\n  async fn post_update(\n    updated: &Self,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Self::post_create(updated, update).await\n  }\n\n  // RENAME\n\n  fn rename_operation() -> Operation {\n    Operation::RenameProcedure\n  }\n\n  // DELETE\n\n  fn delete_operation() -> Operation {\n    Operation::DeleteProcedure\n  }\n\n  async fn pre_delete(\n    _resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  async fn post_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    cancel_schedule(&ResourceTarget::Procedure(resource.id.clone()));\n    procedure_state_cache().remove(&resource.id).await;\n    Ok(())\n  }\n}\n\n#[instrument(skip(user))]\nasync fn validate_config(\n  config: &mut PartialProcedureConfig,\n  user: &User,\n  id: Option<&str>,\n) -> anyhow::Result<()> {\n  let Some(stages) = &mut config.stages else {\n    return Ok(());\n  };\n  for stage in stages {\n    for exec in &mut stage.executions {\n      match &mut exec.execution {\n        Execution::None(_) => {}\n        Execution::RunProcedure(params) => {\n          let procedure = super::get_check_permissions::<Procedure>(\n            &params.procedure,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          match id {\n            Some(id) if procedure.id == id => {\n              return Err(anyhow!(\n                \"Cannot have self-referential procedure\"\n              ));\n            }\n            _ => {}\n          }\n          params.procedure = procedure.id;\n        }\n        Execution::BatchRunProcedure(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::RunAction(params) => {\n          let action = super::get_check_permissions::<Action>(\n            &params.action,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.action = action.id;\n        }\n        Execution::BatchRunAction(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::RunBuild(params) => {\n          let build = super::get_check_permissions::<Build>(\n            &params.build,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.build = build.id;\n        }\n        Execution::BatchRunBuild(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::CancelBuild(params) => {\n          let build = super::get_check_permissions::<Build>(\n            &params.build,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.build = build.id;\n        }\n        Execution::Deploy(params) => {\n          let deployment =\n            super::get_check_permissions::<Deployment>(\n              &params.deployment,\n              user,\n              PermissionLevel::Execute.into(),\n            )\n            .await?;\n          params.deployment = deployment.id;\n        }\n        Execution::BatchDeploy(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::PullDeployment(params) => {\n          let deployment =\n            super::get_check_permissions::<Deployment>(\n              &params.deployment,\n              user,\n              PermissionLevel::Execute.into(),\n            )\n            .await?;\n          params.deployment = deployment.id;\n        }\n        Execution::StartDeployment(params) => {\n          let deployment =\n            super::get_check_permissions::<Deployment>(\n              &params.deployment,\n              user,\n              PermissionLevel::Execute.into(),\n            )\n            .await?;\n          params.deployment = deployment.id;\n        }\n        Execution::RestartDeployment(params) => {\n          let deployment =\n            super::get_check_permissions::<Deployment>(\n              &params.deployment,\n              user,\n              PermissionLevel::Execute.into(),\n            )\n            .await?;\n          params.deployment = deployment.id;\n        }\n        Execution::PauseDeployment(params) => {\n          let deployment =\n            super::get_check_permissions::<Deployment>(\n              &params.deployment,\n              user,\n              PermissionLevel::Execute.into(),\n            )\n            .await?;\n          params.deployment = deployment.id;\n        }\n        Execution::UnpauseDeployment(params) => {\n          let deployment =\n            super::get_check_permissions::<Deployment>(\n              &params.deployment,\n              user,\n              PermissionLevel::Execute.into(),\n            )\n            .await?;\n          params.deployment = deployment.id;\n        }\n        Execution::StopDeployment(params) => {\n          let deployment =\n            super::get_check_permissions::<Deployment>(\n              &params.deployment,\n              user,\n              PermissionLevel::Execute.into(),\n            )\n            .await?;\n          params.deployment = deployment.id;\n        }\n        Execution::DestroyDeployment(params) => {\n          let deployment =\n            super::get_check_permissions::<Deployment>(\n              &params.deployment,\n              user,\n              PermissionLevel::Execute.into(),\n            )\n            .await?;\n          params.deployment = deployment.id;\n        }\n        Execution::BatchDestroyDeployment(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::CloneRepo(params) => {\n          let repo = super::get_check_permissions::<Repo>(\n            &params.repo,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.repo = repo.id;\n        }\n        Execution::BatchCloneRepo(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::PullRepo(params) => {\n          let repo = super::get_check_permissions::<Repo>(\n            &params.repo,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.repo = repo.id;\n        }\n        Execution::BatchPullRepo(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::BuildRepo(params) => {\n          let repo = super::get_check_permissions::<Repo>(\n            &params.repo,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.repo = repo.id;\n        }\n        Execution::BatchBuildRepo(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::CancelRepoBuild(params) => {\n          let repo = super::get_check_permissions::<Repo>(\n            &params.repo,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.repo = repo.id;\n        }\n        Execution::StartContainer(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::RestartContainer(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::PauseContainer(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::UnpauseContainer(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::StopContainer(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::DestroyContainer(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::StartAllContainers(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::RestartAllContainers(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::PauseAllContainers(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::UnpauseAllContainers(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::StopAllContainers(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::PruneContainers(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::DeleteNetwork(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::PruneNetworks(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::DeleteImage(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::PruneImages(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::DeleteVolume(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::PruneVolumes(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::PruneDockerBuilders(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::PruneBuildx(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::PruneSystem(params) => {\n          let server = super::get_check_permissions::<Server>(\n            &params.server,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.server = server.id;\n        }\n        Execution::RunSync(params) => {\n          let sync = super::get_check_permissions::<ResourceSync>(\n            &params.sync,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.sync = sync.id;\n        }\n        Execution::CommitSync(params) => {\n          // This one is actually a write operation.\n          let sync = super::get_check_permissions::<ResourceSync>(\n            &params.sync,\n            user,\n            PermissionLevel::Write.into(),\n          )\n          .await?;\n          params.sync = sync.id;\n        }\n        Execution::DeployStack(params) => {\n          let stack = super::get_check_permissions::<Stack>(\n            &params.stack,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.stack = stack.id;\n        }\n        Execution::BatchDeployStack(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::DeployStackIfChanged(params) => {\n          let stack = super::get_check_permissions::<Stack>(\n            &params.stack,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.stack = stack.id;\n        }\n        Execution::BatchDeployStackIfChanged(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::PullStack(params) => {\n          let stack = super::get_check_permissions::<Stack>(\n            &params.stack,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.stack = stack.id;\n        }\n        Execution::BatchPullStack(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::StartStack(params) => {\n          let stack = super::get_check_permissions::<Stack>(\n            &params.stack,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.stack = stack.id;\n        }\n        Execution::RestartStack(params) => {\n          let stack = super::get_check_permissions::<Stack>(\n            &params.stack,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.stack = stack.id;\n        }\n        Execution::PauseStack(params) => {\n          let stack = super::get_check_permissions::<Stack>(\n            &params.stack,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.stack = stack.id;\n        }\n        Execution::UnpauseStack(params) => {\n          let stack = super::get_check_permissions::<Stack>(\n            &params.stack,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.stack = stack.id;\n        }\n        Execution::StopStack(params) => {\n          let stack = super::get_check_permissions::<Stack>(\n            &params.stack,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.stack = stack.id;\n        }\n        Execution::DestroyStack(params) => {\n          let stack = super::get_check_permissions::<Stack>(\n            &params.stack,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.stack = stack.id;\n        }\n        Execution::RunStackService(params) => {\n          let stack = super::get_check_permissions::<Stack>(\n            &params.stack,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.stack = stack.id;\n        }\n        Execution::BatchDestroyStack(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot configure Batch executions\"\n            ));\n          }\n        }\n        Execution::TestAlerter(params) => {\n          let alerter = super::get_check_permissions::<Alerter>(\n            &params.alerter,\n            user,\n            PermissionLevel::Execute.into(),\n          )\n          .await?;\n          params.alerter = alerter.id;\n        }\n        Execution::SendAlert(params) => {\n          params.alerters = params\n            .alerters\n            .iter()\n            .map(async |alerter| {\n              let id = super::get_check_permissions::<Alerter>(\n                alerter,\n                user,\n                PermissionLevel::Execute.into(),\n              )\n              .await?\n              .id;\n              anyhow::Ok(id)\n            })\n            .collect::<FuturesUnordered<_>>()\n            .try_collect::<Vec<_>>()\n            .await?;\n        }\n        Execution::ClearRepoCache(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot clear repo cache\"\n            ));\n          }\n        }\n        Execution::BackupCoreDatabase(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot trigger core database backup\"\n            ));\n          }\n        }\n        Execution::GlobalAutoUpdate(_params) => {\n          if !user.admin {\n            return Err(anyhow!(\n              \"Non admin user cannot trigger global auto update\"\n            ));\n          }\n        }\n        Execution::Sleep(_) => {}\n      }\n    }\n  }\n\n  Ok(())\n}\n\npub fn spawn_procedure_state_refresh_loop() {\n  tokio::spawn(async move {\n    loop {\n      refresh_procedure_state_cache().await;\n      tokio::time::sleep(Duration::from_secs(60)).await;\n    }\n  });\n}\n\npub async fn refresh_procedure_state_cache() {\n  let _ = async {\n    let procedures =\n      find_collect(&db_client().procedures, None, None)\n        .await\n        .context(\"Failed to get Procedures from db\")?;\n    let cache = procedure_state_cache();\n    for procedure in procedures {\n      let state = get_procedure_state_from_db(&procedure.id).await;\n      cache.insert(procedure.id, state).await;\n    }\n    anyhow::Ok(())\n  }\n  .await\n  .inspect_err(|e| {\n    error!(\"Failed to refresh Procedure state cache | {e:#}\")\n  });\n}\n\nasync fn get_procedure_state_from_db(id: &str) -> ProcedureState {\n  async {\n    let state = db_client()\n      .updates\n      .find_one(doc! {\n        \"target.type\": \"Procedure\",\n        \"target.id\": id,\n        \"operation\": \"RunProcedure\"\n      })\n      .with_options(\n        FindOneOptions::builder()\n          .sort(doc! { \"start_ts\": -1 })\n          .build(),\n      )\n      .await?\n      .map(|u| {\n        if u.success {\n          ProcedureState::Ok\n        } else {\n          ProcedureState::Failed\n        }\n      })\n      .unwrap_or(ProcedureState::Ok);\n    anyhow::Ok(state)\n  }\n  .await\n  .inspect_err(|e| {\n    warn!(\"Failed to get Procedure state for {id} | {e:#}\")\n  })\n  .unwrap_or(ProcedureState::Unknown)\n}\n"
  },
  {
    "path": "bin/core/src/resource/refresh.rs",
    "content": "use std::time::Duration;\n\nuse async_timing_util::{Timelength, get_timelength_in_ms};\nuse database::mungos::find::find_collect;\nuse komodo_client::{\n  api::write::{\n    RefreshBuildCache, RefreshRepoCache, RefreshResourceSyncPending,\n    RefreshStackCache,\n  },\n  entities::user::{build_user, repo_user, stack_user, sync_user},\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  api::write::WriteArgs,\n  config::core_config,\n  helpers::all_resources::AllResourcesById,\n  state::{all_resources_cache, db_client},\n};\n\npub fn spawn_all_resources_cache_refresh_loop() {\n  tokio::spawn(async move {\n    let mut interval = tokio::time::interval(Duration::from_secs(15));\n    loop {\n      interval.tick().await;\n      refresh_all_resources_cache().await;\n    }\n  });\n}\n\npub async fn refresh_all_resources_cache() {\n  let all = match AllResourcesById::load().await {\n    Ok(all) => all,\n    Err(e) => {\n      error!(\"Failed to load all resources by id cache | {e:#}\");\n      return;\n    }\n  };\n  all_resources_cache().store(all.into());\n}\n\npub fn spawn_resource_refresh_loop() {\n  let interval: Timelength = core_config()\n    .resource_poll_interval\n    .try_into()\n    .expect(\"Invalid resource poll interval\");\n  tokio::spawn(async move {\n    let mut interval = tokio::time::interval(Duration::from_millis(\n      get_timelength_in_ms(interval) as u64,\n    ));\n    loop {\n      interval.tick().await;\n      refresh_all().await;\n    }\n  });\n}\n\nasync fn refresh_all() {\n  refresh_stacks().await;\n  refresh_builds().await;\n  refresh_repos().await;\n  refresh_syncs().await;\n}\n\nasync fn refresh_stacks() {\n  let Ok(stacks) = find_collect(&db_client().stacks, None, None)\n    .await\n    .inspect_err(|e| {\n      warn!(\n        \"Failed to get Stacks from database in refresh task | {e:#}\"\n      )\n    })\n  else {\n    return;\n  };\n  for stack in stacks {\n    RefreshStackCache { stack: stack.id }\n      .resolve(\n        &WriteArgs { user: stack_user().clone() },\n      )\n      .await\n      .inspect_err(|e| {\n        warn!(\"Failed to refresh Stack cache in refresh task | Stack: {} | {:#}\", stack.name, e.error)\n      })\n      .ok();\n  }\n}\n\nasync fn refresh_builds() {\n  let Ok(builds) = find_collect(&db_client().builds, None, None)\n    .await\n    .inspect_err(|e| {\n      warn!(\n        \"Failed to get Builds from database in refresh task | {e:#}\"\n      )\n    })\n  else {\n    return;\n  };\n  for build in builds {\n    RefreshBuildCache { build: build.id }\n      .resolve(\n        &WriteArgs { user: build_user().clone() },\n      )\n      .await\n      .inspect_err(|e| {\n        warn!(\"Failed to refresh Build cache in refresh task | Build: {} | {:#}\", build.name, e.error)\n      })\n      .ok();\n  }\n}\n\nasync fn refresh_repos() {\n  let Ok(repos) = find_collect(&db_client().repos, None, None)\n    .await\n    .inspect_err(|e| {\n      warn!(\n        \"Failed to get Repos from database in refresh task | {e:#}\"\n      )\n    })\n  else {\n    return;\n  };\n  for repo in repos {\n    RefreshRepoCache { repo: repo.id }\n      .resolve(\n        &WriteArgs { user: repo_user().clone() },\n      )\n      .await\n      .inspect_err(|e| {\n        warn!(\"Failed to refresh Repo cache in refresh task | Repo: {} | {:#}\", repo.name, e.error)\n      })\n      .ok();\n  }\n}\n\nasync fn refresh_syncs() {\n  let Ok(syncs) = find_collect(\n    &db_client().resource_syncs,\n    None,\n    None,\n  )\n  .await\n  .inspect_err(|e| {\n    warn!(\n      \"failed to get resource syncs from db in refresh task | {e:#}\"\n    )\n  }) else {\n    return;\n  };\n  for sync in syncs {\n    RefreshResourceSyncPending { sync: sync.id }\n      .resolve(\n        &WriteArgs { user: sync_user().clone() },\n      )\n      .await\n      .inspect_err(|e| {\n        warn!(\"Failed to refresh ResourceSync in refresh task | Sync: {} | {:#}\", sync.name, e.error)\n      })\n      .ok();\n  }\n}\n"
  },
  {
    "path": "bin/core/src/resource/repo.rs",
    "content": "use std::time::Duration;\n\nuse anyhow::Context;\nuse database::mungos::{\n  find::find_collect,\n  mongodb::{Collection, bson::doc, options::FindOneOptions},\n};\nuse formatting::format_serror;\nuse komodo_client::entities::{\n  Operation, ResourceTarget, ResourceTargetVariant,\n  builder::Builder,\n  permission::PermissionLevel,\n  repo::{\n    PartialRepoConfig, Repo, RepoConfig, RepoConfigDiff, RepoInfo,\n    RepoListItem, RepoListItemInfo, RepoQuerySpecifics, RepoState,\n  },\n  resource::Resource,\n  server::Server,\n  to_path_compatible_name,\n  update::Update,\n  user::User,\n};\nuse periphery_client::api::git::DeleteRepo;\n\nuse crate::{\n  config::core_config,\n  helpers::{periphery_client, repo_link},\n  state::{\n    action_states, db_client, repo_state_cache, repo_status_cache,\n  },\n};\n\nuse super::get_check_permissions;\n\nimpl super::KomodoResource for Repo {\n  type Config = RepoConfig;\n  type PartialConfig = PartialRepoConfig;\n  type ConfigDiff = RepoConfigDiff;\n  type Info = RepoInfo;\n  type ListItem = RepoListItem;\n  type QuerySpecifics = RepoQuerySpecifics;\n\n  fn resource_type() -> ResourceTargetVariant {\n    ResourceTargetVariant::Repo\n  }\n\n  fn resource_target(id: impl Into<String>) -> ResourceTarget {\n    ResourceTarget::Repo(id.into())\n  }\n\n  fn validated_name(name: &str) -> String {\n    to_path_compatible_name(name)\n  }\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>\n  {\n    &db_client().repos\n  }\n\n  async fn to_list_item(\n    repo: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem {\n    let state = get_repo_state(&repo.id).await;\n    let status =\n      repo_status_cache().get(&repo.id).await.unwrap_or_default();\n    RepoListItem {\n      name: repo.name,\n      id: repo.id,\n      template: repo.template,\n      tags: repo.tags,\n      resource_type: ResourceTargetVariant::Repo,\n      info: RepoListItemInfo {\n        server_id: repo.config.server_id,\n        builder_id: repo.config.builder_id,\n        last_pulled_at: repo.info.last_pulled_at,\n        last_built_at: repo.info.last_built_at,\n        repo_link: repo_link(\n          &repo.config.git_provider,\n          &repo.config.repo,\n          &repo.config.branch,\n          repo.config.git_https,\n        ),\n        git_provider: repo.config.git_provider,\n        repo: repo.config.repo,\n        branch: repo.config.branch,\n        state,\n        cloned_hash: status.latest_hash.clone(),\n        cloned_message: status.latest_message.clone(),\n        latest_hash: repo.info.latest_hash,\n        built_hash: repo.info.built_hash,\n      },\n    }\n  }\n\n  async fn busy(id: &String) -> anyhow::Result<bool> {\n    action_states()\n      .repo\n      .get(id)\n      .await\n      .unwrap_or_default()\n      .busy()\n  }\n\n  // CREATE\n\n  fn create_operation() -> Operation {\n    Operation::CreateRepo\n  }\n\n  fn user_can_create(user: &User) -> bool {\n    user.admin || !core_config().disable_non_admin_create\n  }\n\n  async fn validate_create_config(\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_create(\n    _created: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    refresh_repo_state_cache().await;\n    Ok(())\n  }\n\n  // UPDATE\n\n  fn update_operation() -> Operation {\n    Operation::UpdateRepo\n  }\n\n  async fn validate_update_config(\n    _id: &str,\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_update(\n    _updated: &Self,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    refresh_repo_state_cache().await;\n    Ok(())\n  }\n\n  // RENAME\n\n  fn rename_operation() -> Operation {\n    Operation::RenameRepo\n  }\n\n  // DELETE\n\n  fn delete_operation() -> Operation {\n    Operation::DeleteRepo\n  }\n\n  async fn pre_delete(\n    repo: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    if repo.config.server_id.is_empty() {\n      return Ok(());\n    }\n\n    let server = super::get::<Server>(&repo.config.server_id).await?;\n    let periphery = periphery_client(&server)?;\n\n    match periphery\n      .request(DeleteRepo {\n        name: if repo.config.path.is_empty() {\n          to_path_compatible_name(&repo.name)\n        } else {\n          repo.config.path.clone()\n        },\n        is_build: false,\n      })\n      .await\n    {\n      Ok(log) => update.logs.push(log),\n      Err(e) => update.push_error_log(\n        \"Delete Repo on Periphery\",\n        format_serror(\n          &e.context(\"Failed to delete repo files\").into(),\n        ),\n      ),\n    }\n\n    Ok(())\n  }\n\n  async fn post_delete(\n    repo: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    repo_state_cache().remove(&repo.id).await;\n\n    Ok(())\n  }\n}\n\npub fn spawn_repo_state_refresh_loop() {\n  tokio::spawn(async move {\n    loop {\n      refresh_repo_state_cache().await;\n      tokio::time::sleep(Duration::from_secs(60)).await;\n    }\n  });\n}\n\npub async fn refresh_repo_state_cache() {\n  let _ = async {\n    let repos = find_collect(&db_client().repos, None, None)\n      .await\n      .context(\"failed to get repos from db\")?;\n    let cache = repo_state_cache();\n    for repo in repos {\n      let state = get_repo_state_from_db(&repo.id).await;\n      cache.insert(repo.id, state).await;\n    }\n    anyhow::Ok(())\n  }\n  .await\n  .inspect_err(|e| {\n    warn!(\"failed to refresh repo state cache | {e:#}\")\n  });\n}\n\n#[instrument(skip(user))]\nasync fn validate_config(\n  config: &mut PartialRepoConfig,\n  user: &User,\n) -> anyhow::Result<()> {\n  if let Some(server_id) = &config.server_id\n    && !server_id.is_empty()\n  {\n    let server = get_check_permissions::<Server>(\n      server_id,\n      user,\n      PermissionLevel::Read.attach(),\n    )\n    .await\n    .context(\"Cannot attach Repo to this Server\")?;\n    config.server_id = Some(server.id);\n  }\n  if let Some(builder_id) = &config.builder_id\n    && !builder_id.is_empty()\n  {\n    let builder = super::get_check_permissions::<Builder>(\n      builder_id,\n      user,\n      PermissionLevel::Read.attach(),\n    )\n    .await\n    .context(\"Cannot attach Repo to this Builder\")?;\n    config.builder_id = Some(builder.id);\n  }\n  Ok(())\n}\n\nasync fn get_repo_state(id: &String) -> RepoState {\n  if let Some(state) = action_states()\n    .repo\n    .get(id)\n    .await\n    .and_then(|s| {\n      s.get()\n        .map(|s| {\n          if s.cloning {\n            Some(RepoState::Cloning)\n          } else if s.pulling {\n            Some(RepoState::Pulling)\n          } else if s.building {\n            Some(RepoState::Building)\n          } else {\n            None\n          }\n        })\n        .ok()\n    })\n    .flatten()\n  {\n    return state;\n  }\n  repo_state_cache().get(id).await.unwrap_or_default()\n}\n\nasync fn get_repo_state_from_db(id: &str) -> RepoState {\n  async {\n    let state = db_client()\n      .updates\n      .find_one(doc! {\n        \"target.type\": \"Repo\",\n        \"target.id\": id,\n        \"$or\": [\n          { \"operation\": \"CloneRepo\" },\n          { \"operation\": \"PullRepo\" },\n          { \"operation\": \"BuildRepo\" },\n        ],\n      })\n      .with_options(\n        FindOneOptions::builder()\n          .sort(doc! { \"start_ts\": -1 })\n          .build(),\n      )\n      .await?\n      .map(|u| {\n        if u.success {\n          RepoState::Ok\n        } else {\n          RepoState::Failed\n        }\n      })\n      .unwrap_or(RepoState::Ok);\n    anyhow::Ok(state)\n  }\n  .await\n  .inspect_err(|e| warn!(\"failed to get repo state for {id} | {e:#}\"))\n  .unwrap_or(RepoState::Unknown)\n}\n"
  },
  {
    "path": "bin/core/src/resource/server.rs",
    "content": "use anyhow::Context;\nuse database::mungos::mongodb::{Collection, bson::doc};\nuse indexmap::IndexSet;\nuse komodo_client::entities::{\n  Operation, ResourceTarget, ResourceTargetVariant, komodo_timestamp,\n  permission::SpecificPermission,\n  resource::Resource,\n  server::{\n    PartialServerConfig, Server, ServerConfig, ServerConfigDiff,\n    ServerListItem, ServerListItemInfo, ServerQuerySpecifics,\n  },\n  update::Update,\n  user::User,\n};\n\nuse crate::{\n  config::core_config,\n  helpers::query::get_system_info,\n  monitor::update_cache_for_server,\n  state::{action_states, db_client, server_status_cache},\n};\n\nimpl super::KomodoResource for Server {\n  type Config = ServerConfig;\n  type PartialConfig = PartialServerConfig;\n  type ConfigDiff = ServerConfigDiff;\n  type Info = ();\n  type ListItem = ServerListItem;\n  type QuerySpecifics = ServerQuerySpecifics;\n\n  fn resource_type() -> ResourceTargetVariant {\n    ResourceTargetVariant::Server\n  }\n\n  fn resource_target(id: impl Into<String>) -> ResourceTarget {\n    ResourceTarget::Server(id.into())\n  }\n\n  fn creator_specific_permissions() -> IndexSet<SpecificPermission> {\n    [\n      SpecificPermission::Terminal,\n      SpecificPermission::Inspect,\n      SpecificPermission::Attach,\n      SpecificPermission::Logs,\n      SpecificPermission::Processes,\n    ]\n    .into_iter()\n    .collect()\n  }\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>\n  {\n    &db_client().servers\n  }\n\n  async fn to_list_item(\n    server: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem {\n    let status = server_status_cache().get(&server.id).await;\n    let (terminals_disabled, container_exec_disabled) =\n      get_system_info(&server)\n        .await\n        .map(|i| (i.terminals_disabled, i.container_exec_disabled))\n        .unwrap_or((true, true));\n    ServerListItem {\n      name: server.name,\n      id: server.id,\n      template: server.template,\n      tags: server.tags,\n      resource_type: ResourceTargetVariant::Server,\n      info: ServerListItemInfo {\n        state: status.as_ref().map(|s| s.state).unwrap_or_default(),\n        version: status\n          .map(|s| s.version.clone())\n          .unwrap_or(String::from(\"Unknown\")),\n        region: server.config.region,\n        address: server.config.address,\n        external_address: server.config.external_address,\n        send_unreachable_alerts: server\n          .config\n          .send_unreachable_alerts,\n        send_cpu_alerts: server.config.send_cpu_alerts,\n        send_mem_alerts: server.config.send_mem_alerts,\n        send_disk_alerts: server.config.send_disk_alerts,\n        send_version_mismatch_alerts: server\n          .config\n          .send_version_mismatch_alerts,\n        terminals_disabled,\n        container_exec_disabled,\n      },\n    }\n  }\n\n  async fn busy(id: &String) -> anyhow::Result<bool> {\n    action_states()\n      .server\n      .get(id)\n      .await\n      .unwrap_or_default()\n      .busy()\n  }\n\n  // CREATE\n\n  fn create_operation() -> Operation {\n    Operation::CreateServer\n  }\n\n  fn user_can_create(user: &User) -> bool {\n    user.admin\n      || (!core_config().disable_non_admin_create\n        && user.create_server_permissions)\n  }\n\n  async fn validate_create_config(\n    _config: &mut Self::PartialConfig,\n    _user: &User,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  async fn post_create(\n    created: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    update_cache_for_server(created, true).await;\n    Ok(())\n  }\n\n  // UPDATE\n\n  fn update_operation() -> Operation {\n    Operation::UpdateServer\n  }\n\n  async fn validate_update_config(\n    _id: &str,\n    _config: &mut Self::PartialConfig,\n    _user: &User,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n\n  async fn post_update(\n    updated: &Self,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    update_cache_for_server(updated, true).await;\n    Ok(())\n  }\n\n  // RENAME\n\n  fn rename_operation() -> Operation {\n    Operation::RenameServer\n  }\n\n  // DELETE\n\n  fn delete_operation() -> Operation {\n    Operation::DeleteServer\n  }\n\n  async fn pre_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    let db = db_client();\n\n    let id = &resource.id;\n\n    db.builders\n      .update_many(\n        doc! { \"config.params.server_id\": &id },\n        doc! { \"$set\": { \"config.params.server_id\": \"\" } },\n      )\n      .await\n      .context(\"failed to detach server from builders\")?;\n\n    db.deployments\n      .update_many(\n        doc! { \"config.server_id\": &id },\n        doc! { \"$set\": { \"config.server_id\": \"\" } },\n      )\n      .await\n      .context(\"failed to detach server from deployments\")?;\n\n    db.stacks\n      .update_many(\n        doc! { \"config.server_id\": &id },\n        doc! { \"$set\": { \"config.server_id\": \"\" } },\n      )\n      .await\n      .context(\"failed to detach server from stacks\")?;\n\n    db.repos\n      .update_many(\n        doc! { \"config.server_id\": &id },\n        doc! { \"$set\": { \"config.server_id\": \"\" } },\n      )\n      .await\n      .context(\"failed to detach server from repos\")?;\n\n    db.alerts\n      .update_many(\n        doc! { \"target.type\": \"Server\", \"target.id\": &id },\n        doc! { \"$set\": {\n          \"resolved\": true,\n          \"resolved_ts\": komodo_timestamp()\n        } },\n      )\n      .await\n      .context(\"failed to close deleted server alerts\")?;\n\n    Ok(())\n  }\n\n  async fn post_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    server_status_cache().remove(&resource.id).await;\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "bin/core/src/resource/stack.rs",
    "content": "use anyhow::Context;\nuse database::mungos::mongodb::Collection;\nuse formatting::format_serror;\nuse indexmap::IndexSet;\nuse komodo_client::{\n  api::write::RefreshStackCache,\n  entities::{\n    Operation, ResourceTarget, ResourceTargetVariant,\n    permission::{PermissionLevel, SpecificPermission},\n    repo::Repo,\n    resource::Resource,\n    server::Server,\n    stack::{\n      PartialStackConfig, Stack, StackConfig, StackConfigDiff,\n      StackInfo, StackListItem, StackListItemInfo,\n      StackQuerySpecifics, StackServiceWithUpdate, StackState,\n    },\n    to_docker_compatible_name,\n    update::Update,\n    user::{User, stack_user},\n  },\n};\nuse periphery_client::api::compose::ComposeExecution;\nuse resolver_api::Resolve;\n\nuse crate::{\n  api::write::WriteArgs,\n  config::core_config,\n  helpers::{periphery_client, query::get_stack_state, repo_link},\n  monitor::update_cache_for_server,\n  state::{\n    action_states, all_resources_cache, db_client,\n    server_status_cache, stack_status_cache,\n  },\n};\n\nuse super::get_check_permissions;\n\nimpl super::KomodoResource for Stack {\n  type Config = StackConfig;\n  type PartialConfig = PartialStackConfig;\n  type ConfigDiff = StackConfigDiff;\n  type Info = StackInfo;\n  type ListItem = StackListItem;\n  type QuerySpecifics = StackQuerySpecifics;\n\n  fn resource_type() -> ResourceTargetVariant {\n    ResourceTargetVariant::Stack\n  }\n\n  fn resource_target(id: impl Into<String>) -> ResourceTarget {\n    ResourceTarget::Stack(id.into())\n  }\n\n  fn validated_name(name: &str) -> String {\n    to_docker_compatible_name(name)\n  }\n\n  fn creator_specific_permissions() -> IndexSet<SpecificPermission> {\n    [\n      SpecificPermission::Inspect,\n      SpecificPermission::Logs,\n      SpecificPermission::Terminal,\n    ]\n    .into_iter()\n    .collect()\n  }\n\n  fn inherit_specific_permissions_from(\n    _self: &Resource<Self::Config, Self::Info>,\n  ) -> Option<ResourceTarget> {\n    ResourceTarget::Server(_self.config.server_id.clone()).into()\n  }\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>\n  {\n    &db_client().stacks\n  }\n\n  async fn to_list_item(\n    stack: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem {\n    let status = stack_status_cache().get(&stack.id).await;\n    let state = if action_states()\n      .stack\n      .get(&stack.id)\n      .await\n      .map(|s| s.get().map(|s| s.deploying))\n      .transpose()\n      .ok()\n      .flatten()\n      .unwrap_or_default()\n    {\n      StackState::Deploying\n    } else {\n      status.as_ref().map(|s| s.curr.state).unwrap_or_default()\n    };\n    let project_name = stack.project_name(false);\n    let services = status\n      .as_ref()\n      .map(|s| {\n        s.curr\n          .services\n          .iter()\n          .map(|service| StackServiceWithUpdate {\n            service: service.service.clone(),\n            image: service.image.clone(),\n            update_available: service.update_available,\n          })\n          .collect::<Vec<_>>()\n      })\n      .unwrap_or_default();\n\n    let default_git = (\n      stack.config.git_provider,\n      stack.config.repo,\n      stack.config.branch,\n      stack.config.git_https,\n    );\n    let (git_provider, repo, branch, git_https) =\n      if stack.config.linked_repo.is_empty() {\n        default_git\n      } else {\n        all_resources_cache()\n          .load()\n          .repos\n          .get(&stack.config.linked_repo)\n          .map(|r| {\n            (\n              r.config.git_provider.clone(),\n              r.config.repo.clone(),\n              r.config.branch.clone(),\n              r.config.git_https,\n            )\n          })\n          .unwrap_or(default_git)\n      };\n\n    // This is only true if it is KNOWN to be true. so other cases are false.\n    let (project_missing, status) =\n      if stack.config.server_id.is_empty()\n        || matches!(state, StackState::Down | StackState::Unknown)\n      {\n        (false, None)\n      } else if let Some(status) = server_status_cache()\n        .get(&stack.config.server_id)\n        .await\n        .as_ref()\n      {\n        if let Some(projects) = &status.projects {\n          if let Some(project) = projects\n            .iter()\n            .find(|project| project.name == project_name)\n          {\n            (false, project.status.clone())\n          } else {\n            // The project doesn't exist\n            (true, None)\n          }\n        } else {\n          (false, None)\n        }\n      } else {\n        (false, None)\n      };\n\n    StackListItem {\n      name: stack.name,\n      id: stack.id,\n      template: stack.template,\n      tags: stack.tags,\n      resource_type: ResourceTargetVariant::Stack,\n      info: StackListItemInfo {\n        state,\n        status,\n        services,\n        project_missing,\n        file_contents: !stack.config.file_contents.is_empty(),\n        server_id: stack.config.server_id,\n        linked_repo: stack.config.linked_repo,\n        missing_files: stack.info.missing_files,\n        files_on_host: stack.config.files_on_host,\n        repo_link: repo_link(\n          &git_provider,\n          &repo,\n          &branch,\n          git_https,\n        ),\n        git_provider,\n        repo,\n        branch,\n        latest_hash: stack.info.latest_hash,\n        deployed_hash: stack.info.deployed_hash,\n      },\n    }\n  }\n\n  async fn busy(id: &String) -> anyhow::Result<bool> {\n    action_states()\n      .stack\n      .get(id)\n      .await\n      .unwrap_or_default()\n      .busy()\n  }\n\n  // CREATE\n\n  fn create_operation() -> Operation {\n    Operation::CreateStack\n  }\n\n  fn user_can_create(user: &User) -> bool {\n    user.admin || !core_config().disable_non_admin_create\n  }\n\n  async fn validate_create_config(\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_create(\n    created: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    if let Err(e) = (RefreshStackCache {\n      stack: created.name.clone(),\n    })\n    .resolve(&WriteArgs {\n      user: stack_user().to_owned(),\n    })\n    .await\n    {\n      update.push_error_log(\n        \"Refresh stack cache\",\n        format_serror(&e.error.context(\"The stack cache has failed to refresh. This may be due to a misconfiguration of the Stack\").into())\n      );\n    };\n    if created.config.server_id.is_empty() {\n      return Ok(());\n    }\n    let Ok(server) = super::get::<Server>(&created.config.server_id)\n      .await\n      .inspect_err(|e| {\n        warn!(\n          \"Failed to get Server for Stack {} | {e:#}\",\n          created.name\n        )\n      })\n    else {\n      return Ok(());\n    };\n    update_cache_for_server(&server, true).await;\n    Ok(())\n  }\n\n  // UPDATE\n\n  fn update_operation() -> Operation {\n    Operation::UpdateStack\n  }\n\n  async fn validate_update_config(\n    _id: &str,\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_update(\n    updated: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Self::post_create(updated, update).await\n  }\n\n  // RENAME\n\n  fn rename_operation() -> Operation {\n    Operation::RenameStack\n  }\n\n  // DELETE\n\n  fn delete_operation() -> Operation {\n    Operation::DeleteStack\n  }\n\n  async fn pre_delete(\n    stack: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    // If it is Up, it should be taken down\n    let state = get_stack_state(stack)\n      .await\n      .context(\"failed to get stack state\")?;\n    if matches!(state, StackState::Down | StackState::Unknown) {\n      return Ok(());\n    }\n    // stack needs to be destroyed\n    let server =\n      match super::get::<Server>(&stack.config.server_id).await {\n        Ok(server) => server,\n        Err(e) => {\n          update.push_error_log(\n            \"destroy stack\",\n            format_serror(\n              &e.context(format!(\n                \"failed to retrieve server at {} from db.\",\n                stack.config.server_id\n              ))\n              .into(),\n            ),\n          );\n          return Ok(());\n        }\n      };\n\n    if !server.config.enabled {\n      update.push_simple_log(\n        \"destroy stack\",\n        \"skipping stack destroy, server is disabled.\",\n      );\n      return Ok(());\n    }\n\n    let periphery = match periphery_client(&server) {\n      Ok(periphery) => periphery,\n      Err(e) => {\n        // This case won't ever happen, as periphery_client only fallible if the server is disabled.\n        // Leaving it for completeness sake\n        update.push_error_log(\n          \"destroy stack\",\n          format_serror(\n            &e.context(\"failed to get periphery client\").into(),\n          ),\n        );\n        return Ok(());\n      }\n    };\n\n    match periphery\n      .request(ComposeExecution {\n        project: stack.project_name(false),\n        command: String::from(\"down --remove-orphans\"),\n      })\n      .await\n    {\n      Ok(log) => update.logs.push(log),\n      Err(e) => update.push_simple_log(\n        \"Failed to destroy stack\",\n        format_serror(\n          &e.context(\n            \"failed to destroy stack on periphery server before delete\",\n          )\n          .into(),\n        ),\n      ),\n    };\n\n    Ok(())\n  }\n\n  async fn post_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    stack_status_cache().remove(&resource.id).await;\n    Ok(())\n  }\n}\n\n#[instrument(skip(user))]\nasync fn validate_config(\n  config: &mut PartialStackConfig,\n  user: &User,\n) -> anyhow::Result<()> {\n  if let Some(server_id) = &config.server_id\n    && !server_id.is_empty()\n  {\n    let server = get_check_permissions::<Server>(\n      server_id,\n      user,\n      PermissionLevel::Read.attach(),\n    )\n    .await\n    .context(\"Cannot attach Stack to this Server\")?;\n    // in case it comes in as name\n    config.server_id = Some(server.id);\n  }\n  if let Some(linked_repo) = &config.linked_repo\n    && !linked_repo.is_empty()\n  {\n    let repo = get_check_permissions::<Repo>(\n      linked_repo,\n      user,\n      PermissionLevel::Read.attach(),\n    )\n    .await\n    .context(\"Cannot attach Repo to this Stack\")?;\n    // in case it comes in as name\n    config.linked_repo = Some(repo.id);\n  }\n  Ok(())\n}\n"
  },
  {
    "path": "bin/core/src/resource/sync.rs",
    "content": "use anyhow::Context;\nuse database::mongo_indexed::doc;\nuse database::mungos::mongodb::Collection;\nuse formatting::format_serror;\nuse komodo_client::{\n  api::write::RefreshResourceSyncPending,\n  entities::{\n    Operation, ResourceTarget, ResourceTargetVariant,\n    komodo_timestamp,\n    permission::PermissionLevel,\n    repo::Repo,\n    resource::Resource,\n    sync::{\n      PartialResourceSyncConfig, ResourceSync, ResourceSyncConfig,\n      ResourceSyncConfigDiff, ResourceSyncInfo, ResourceSyncListItem,\n      ResourceSyncListItemInfo, ResourceSyncQuerySpecifics,\n      ResourceSyncState,\n    },\n    update::Update,\n    user::{User, sync_user},\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  api::write::WriteArgs,\n  helpers::repo_link,\n  permission::get_check_permissions,\n  state::{action_states, all_resources_cache, db_client},\n};\n\nimpl super::KomodoResource for ResourceSync {\n  type Config = ResourceSyncConfig;\n  type PartialConfig = PartialResourceSyncConfig;\n  type ConfigDiff = ResourceSyncConfigDiff;\n  type Info = ResourceSyncInfo;\n  type ListItem = ResourceSyncListItem;\n  type QuerySpecifics = ResourceSyncQuerySpecifics;\n\n  fn resource_type() -> ResourceTargetVariant {\n    ResourceTargetVariant::ResourceSync\n  }\n\n  fn resource_target(id: impl Into<String>) -> ResourceTarget {\n    ResourceTarget::ResourceSync(id.into())\n  }\n\n  fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>\n  {\n    &db_client().resource_syncs\n  }\n\n  async fn to_list_item(\n    resource_sync: Resource<Self::Config, Self::Info>,\n  ) -> Self::ListItem {\n    let state =\n      get_resource_sync_state(&resource_sync.id, &resource_sync.info)\n        .await;\n\n    let default_git = (\n      resource_sync.config.git_provider,\n      resource_sync.config.repo,\n      resource_sync.config.branch,\n      resource_sync.config.git_https,\n    );\n    let (git_provider, repo, branch, git_https) =\n      if resource_sync.config.linked_repo.is_empty() {\n        default_git\n      } else {\n        all_resources_cache()\n          .load()\n          .repos\n          .get(&resource_sync.config.linked_repo)\n          .map(|r| {\n            (\n              r.config.git_provider.clone(),\n              r.config.repo.clone(),\n              r.config.branch.clone(),\n              r.config.git_https,\n            )\n          })\n          .unwrap_or(default_git)\n      };\n\n    ResourceSyncListItem {\n      name: resource_sync.name,\n      id: resource_sync.id,\n      template: resource_sync.template,\n      tags: resource_sync.tags,\n      resource_type: ResourceTargetVariant::ResourceSync,\n      info: ResourceSyncListItemInfo {\n        file_contents: !resource_sync.config.file_contents.is_empty(),\n        files_on_host: resource_sync.config.files_on_host,\n        managed: resource_sync.config.managed,\n        linked_repo: resource_sync.config.linked_repo,\n        repo_link: repo_link(\n          &git_provider,\n          &repo,\n          &branch,\n          git_https,\n        ),\n        git_provider,\n        repo,\n        branch,\n        last_sync_ts: resource_sync.info.last_sync_ts,\n        last_sync_hash: resource_sync.info.last_sync_hash,\n        last_sync_message: resource_sync.info.last_sync_message,\n        resource_path: resource_sync.config.resource_path,\n        state,\n      },\n    }\n  }\n\n  async fn busy(id: &String) -> anyhow::Result<bool> {\n    action_states()\n      .sync\n      .get(id)\n      .await\n      .unwrap_or_default()\n      .busy()\n  }\n\n  // CREATE\n\n  fn create_operation() -> Operation {\n    Operation::CreateResourceSync\n  }\n\n  fn user_can_create(user: &User) -> bool {\n    user.admin\n  }\n\n  async fn validate_create_config(\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_create(\n    created: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    if let Err(e) = (RefreshResourceSyncPending {\n      sync: created.id.clone(),\n    })\n    .resolve(&WriteArgs {\n      user: sync_user().to_owned(),\n    })\n    .await\n    {\n      update.push_error_log(\n        \"Refresh sync pending\",\n        format_serror(&e.error.context(\"The sync pending cache has failed to refresh. This is likely due to a misconfiguration of the sync\").into())\n      );\n    };\n    Ok(())\n  }\n\n  // UPDATE\n\n  fn update_operation() -> Operation {\n    Operation::UpdateResourceSync\n  }\n\n  async fn validate_update_config(\n    _id: &str,\n    config: &mut Self::PartialConfig,\n    user: &User,\n  ) -> anyhow::Result<()> {\n    validate_config(config, user).await\n  }\n\n  async fn post_update(\n    updated: &Resource<Self::Config, Self::Info>,\n    update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Self::post_create(updated, update).await\n  }\n\n  // RENAME\n\n  fn rename_operation() -> Operation {\n    Operation::RenameResourceSync\n  }\n\n  // DELETE\n\n  fn delete_operation() -> Operation {\n    Operation::DeleteResourceSync\n  }\n\n  async fn pre_delete(\n    resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    db_client().alerts\n      .update_many(\n        doc! { \"target.type\": \"ResourceSync\", \"target.id\": &resource.id },\n        doc! { \"$set\": {\n          \"resolved\": true,\n          \"resolved_ts\": komodo_timestamp()\n        } },\n      )\n      .await\n      .context(\"failed to close deleted sync alerts\")?;\n\n    Ok(())\n  }\n\n  async fn post_delete(\n    _resource: &Resource<Self::Config, Self::Info>,\n    _update: &mut Update,\n  ) -> anyhow::Result<()> {\n    Ok(())\n  }\n}\n\n#[instrument(skip(user))]\nasync fn validate_config(\n  config: &mut PartialResourceSyncConfig,\n  user: &User,\n) -> anyhow::Result<()> {\n  if let Some(linked_repo) = &config.linked_repo\n    && !linked_repo.is_empty()\n  {\n    let repo = get_check_permissions::<Repo>(\n      linked_repo,\n      user,\n      PermissionLevel::Read.attach(),\n    )\n    .await\n    .context(\"Cannot attach Repo to this Resource Sync\")?;\n    // in case it comes in as name\n    config.linked_repo = Some(repo.id);\n  }\n  Ok(())\n}\n\nasync fn get_resource_sync_state(\n  id: &String,\n  data: &ResourceSyncInfo,\n) -> ResourceSyncState {\n  if let Some(state) = action_states()\n    .sync\n    .get(id)\n    .await\n    .and_then(|s| {\n      s.get()\n        .map(|s| {\n          if s.syncing {\n            Some(ResourceSyncState::Syncing)\n          } else {\n            None\n          }\n        })\n        .ok()\n    })\n    .flatten()\n  {\n    return state;\n  }\n  if data.pending_error.is_some() || !data.remote_errors.is_empty() {\n    ResourceSyncState::Failed\n  } else if !data.resource_updates.is_empty()\n    || !data.variable_updates.is_empty()\n    || !data.user_group_updates.is_empty()\n    || data.pending_deploy.to_deploy > 0\n  {\n    ResourceSyncState::Pending\n  } else {\n    ResourceSyncState::Ok\n  }\n}\n"
  },
  {
    "path": "bin/core/src/schedule.rs",
    "content": "use std::{\n  collections::HashMap,\n  sync::{OnceLock, RwLock},\n};\n\nuse anyhow::{Context, anyhow};\nuse async_timing_util::Timelength;\nuse chrono::Local;\nuse croner::parser::CronParser;\nuse database::mungos::find::find_collect;\nuse formatting::format_serror;\nuse komodo_client::{\n  api::execute::{RunAction, RunProcedure},\n  entities::{\n    ResourceTarget, ResourceTargetVariant, ScheduleFormat,\n    action::Action,\n    alert::{Alert, AlertData, SeverityLevel},\n    komodo_timestamp,\n    procedure::Procedure,\n    user::{action_user, procedure_user},\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  alert::send_alerts,\n  api::execute::{ExecuteArgs, ExecuteRequest},\n  config::core_config,\n  helpers::update::init_execution_update,\n  state::db_client,\n};\n\npub fn spawn_schedule_executor() {\n  // Executor thread\n  tokio::spawn(async move {\n    update_schedules().await;\n    loop {\n      let current_time = async_timing_util::wait_until_timelength(\n        Timelength::OneSecond,\n        0,\n      )\n      .await as i64;\n      let mut lock = schedules().write().unwrap();\n      let drained = lock.drain().collect::<Vec<_>>();\n      for (target, next_run) in drained {\n        match next_run {\n          Ok(next_run_time) if current_time >= next_run_time => {\n            tokio::spawn(async move {\n              match &target {\n                ResourceTarget::Action(id) => {\n                  let action = match crate::resource::get::<Action>(\n                    id,\n                  )\n                  .await\n                  {\n                    Ok(action) => action,\n                    Err(e) => {\n                      warn!(\n                        \"Scheduled action run on {id} failed | failed to get procedure | {e:?}\"\n                      );\n                      return;\n                    }\n                  };\n                  let request =\n                    ExecuteRequest::RunAction(RunAction {\n                      action: id.clone(),\n                      args: Default::default(),\n                    });\n                  let update = match init_execution_update(\n                    &request,\n                    action_user(),\n                  )\n                  .await\n                  {\n                    Ok(update) => update,\n                    Err(e) => {\n                      error!(\n                        \"Failed to make update for scheduled action run, action {id} is not being run | {e:#}\"\n                      );\n                      return;\n                    }\n                  };\n                  let ExecuteRequest::RunAction(request) = request\n                  else {\n                    unreachable!()\n                  };\n                  if let Err(e) = request\n                    .resolve(&ExecuteArgs {\n                      user: action_user().to_owned(),\n                      update,\n                    })\n                    .await\n                  {\n                    warn!(\n                      \"Scheduled action run on {id} failed | {e:?}\"\n                    );\n                  }\n                  update_schedule(&action);\n                  if action.config.schedule_alert {\n                    let alert = Alert {\n                      id: Default::default(),\n                      target,\n                      ts: komodo_timestamp(),\n                      resolved_ts: Some(komodo_timestamp()),\n                      resolved: true,\n                      level: SeverityLevel::Ok,\n                      data: AlertData::ScheduleRun {\n                        resource_type: ResourceTargetVariant::Action,\n                        id: action.id,\n                        name: action.name,\n                      },\n                    };\n                    send_alerts(&[alert]).await\n                  }\n                }\n                ResourceTarget::Procedure(id) => {\n                  let procedure = match crate::resource::get::<\n                    Procedure,\n                  >(id)\n                  .await\n                  {\n                    Ok(procedure) => procedure,\n                    Err(e) => {\n                      warn!(\n                        \"Scheduled procedure run on {id} failed | failed to get procedure | {e:?}\"\n                      );\n                      return;\n                    }\n                  };\n                  let request =\n                    ExecuteRequest::RunProcedure(RunProcedure {\n                      procedure: id.clone(),\n                    });\n                  let update = match init_execution_update(\n                    &request,\n                    procedure_user(),\n                  )\n                  .await\n                  {\n                    Ok(update) => update,\n                    Err(e) => {\n                      error!(\n                        \"Failed to make update for scheduled procedure run, procedure {id} is not being run | {e:#}\"\n                      );\n                      return;\n                    }\n                  };\n                  let ExecuteRequest::RunProcedure(request) = request\n                  else {\n                    unreachable!()\n                  };\n                  if let Err(e) = request\n                    .resolve(&ExecuteArgs {\n                      user: procedure_user().to_owned(),\n                      update,\n                    })\n                    .await\n                  {\n                    warn!(\n                      \"Scheduled procedure run on {id} failed | {e:?}\"\n                    );\n                  }\n                  update_schedule(&procedure);\n                  if procedure.config.schedule_alert {\n                    let alert = Alert {\n                      id: Default::default(),\n                      target,\n                      ts: komodo_timestamp(),\n                      resolved_ts: Some(komodo_timestamp()),\n                      resolved: true,\n                      level: SeverityLevel::Ok,\n                      data: AlertData::ScheduleRun {\n                        resource_type:\n                          ResourceTargetVariant::Procedure,\n                        id: procedure.id,\n                        name: procedure.name,\n                      },\n                    };\n                    send_alerts(&[alert]).await\n                  }\n                }\n                _ => unreachable!(),\n              }\n            });\n          }\n          other => {\n            lock.insert(target, other);\n            continue;\n          }\n        };\n      }\n    }\n  });\n}\n\ntype UnixTimestampMs = i64;\ntype Schedules =\n  HashMap<ResourceTarget, Result<UnixTimestampMs, String>>;\n\nfn schedules() -> &'static RwLock<Schedules> {\n  static SCHEDULES: OnceLock<RwLock<Schedules>> = OnceLock::new();\n  SCHEDULES.get_or_init(Default::default)\n}\n\npub fn get_schedule_item_info(\n  target: &ResourceTarget,\n) -> (Option<i64>, Option<String>) {\n  match schedules().read().unwrap().get(target) {\n    Some(Ok(time)) => (Some(*time), None),\n    Some(Err(e)) => (None, Some(e.clone())),\n    None => (None, None),\n  }\n}\n\npub fn cancel_schedule(target: &ResourceTarget) {\n  schedules().write().unwrap().remove(target);\n}\n\npub async fn update_schedules() {\n  let (procedures, actions) = tokio::join!(\n    find_collect(&db_client().procedures, None, None),\n    find_collect(&db_client().actions, None, None),\n  );\n  let procedures = match procedures\n    .context(\"failed to get all procedures from db\")\n  {\n    Ok(procedures) => procedures,\n    Err(e) => {\n      error!(\"failed to get procedures for schedule update | {e:#}\");\n      Vec::new()\n    }\n  };\n  let actions =\n    match actions.context(\"failed to get all actions from db\") {\n      Ok(actions) => actions,\n      Err(e) => {\n        error!(\"failed to get actions for schedule update | {e:#}\");\n        Vec::new()\n      }\n    };\n  // clear out any schedules which don't match to existing resources\n  {\n    let mut lock = schedules().write().unwrap();\n    lock.retain(|target, _| match target {\n      ResourceTarget::Action(id) => {\n        actions.iter().any(|action| &action.id == id)\n      }\n      ResourceTarget::Procedure(id) => {\n        procedures.iter().any(|procedure| &procedure.id == id)\n      }\n      _ => unreachable!(),\n    });\n  }\n  for procedure in procedures {\n    update_schedule(&procedure);\n  }\n  for action in actions {\n    update_schedule(&action);\n  }\n}\n\n/// Re/spawns the schedule for the given procedure\npub fn update_schedule(schedule: impl HasSchedule) {\n  // Cancel any existing schedule for the procedure\n  cancel_schedule(&schedule.target());\n\n  if !schedule.enabled() || schedule.schedule().is_empty() {\n    return;\n  }\n\n  schedules().write().unwrap().insert(\n    schedule.target(),\n    find_next_occurrence(schedule)\n      .map_err(|e| format_serror(&e.into())),\n  );\n}\n\nfn cron_parser() -> &'static CronParser {\n  static CRON_PARSER: OnceLock<CronParser> = OnceLock::new();\n  CRON_PARSER.get_or_init(|| {\n    CronParser::builder()\n      .seconds(croner::parser::Seconds::Required)\n      .dom_and_dow(true)\n      .build()\n  })\n}\n\n/// Finds the next run occurence in UTC ms.\nfn find_next_occurrence(\n  schedule: impl HasSchedule,\n) -> anyhow::Result<i64> {\n  let cron = match schedule.format() {\n    ScheduleFormat::Cron => cron_parser()\n      .parse(schedule.schedule())\n      .context(\"Invalid CRON schedule\")?,\n    ScheduleFormat::English => {\n      let cron =\n        english_to_cron::str_cron_syntax(schedule.schedule())\n          .map_err(|e| {\n            anyhow!(\"Failed to parse english to cron | {e:?}\")\n          })?\n          .split(' ')\n          // croner does not accept year\n          .take(6)\n          .collect::<Vec<_>>()\n          .join(\" \");\n      cron_parser()\n        .parse(&cron)\n        .with_context(|| format!(\"English expression produced invalid CRON schedule | produced: {cron}\"))?\n    }\n  };\n  let next =\n    match (schedule.timezone(), core_config().timezone.as_str()) {\n      (\"\", \"\") => {\n        let tz_time = chrono::Local::now().with_timezone(&Local);\n        cron\n          .find_next_occurrence(&tz_time, false)\n          .context(\"Failed to find next run time\")?\n          .timestamp_millis()\n      }\n      (\"\", timezone) | (timezone, _) => {\n        let tz: chrono_tz::Tz =\n          timezone.parse().context(\"Failed to parse timezone\")?;\n        let tz_time = chrono::Local::now().with_timezone(&tz);\n        cron\n          .find_next_occurrence(&tz_time, false)\n          .context(\"Failed to find next run time\")?\n          .timestamp_millis()\n      }\n    };\n  Ok(next)\n}\n\npub trait HasSchedule {\n  fn target(&self) -> ResourceTarget;\n  fn enabled(&self) -> bool;\n  fn format(&self) -> ScheduleFormat;\n  fn schedule(&self) -> &str;\n  fn timezone(&self) -> &str;\n}\n\nimpl HasSchedule for &Procedure {\n  fn target(&self) -> ResourceTarget {\n    ResourceTarget::Procedure(self.id.clone())\n  }\n  fn enabled(&self) -> bool {\n    self.config.schedule_enabled\n  }\n  fn format(&self) -> ScheduleFormat {\n    self.config.schedule_format\n  }\n  fn schedule(&self) -> &str {\n    &self.config.schedule\n  }\n  fn timezone(&self) -> &str {\n    &self.config.schedule_timezone\n  }\n}\n\nimpl HasSchedule for &Action {\n  fn target(&self) -> ResourceTarget {\n    ResourceTarget::Action(self.id.clone())\n  }\n  fn enabled(&self) -> bool {\n    self.config.schedule_enabled\n  }\n  fn format(&self) -> ScheduleFormat {\n    self.config.schedule_format\n  }\n  fn schedule(&self) -> &str {\n    &self.config.schedule\n  }\n  fn timezone(&self) -> &str {\n    &self.config.schedule_timezone\n  }\n}\n"
  },
  {
    "path": "bin/core/src/stack/execute.rs",
    "content": "use komodo_client::{\n  api::execute::*,\n  entities::{\n    permission::PermissionLevel,\n    stack::{Stack, StackActionState},\n    update::{Log, Update},\n    user::User,\n  },\n};\nuse periphery_client::{PeripheryClient, api::compose::*};\n\nuse crate::{\n  helpers::{periphery_client, update::update_update},\n  monitor::update_cache_for_server,\n  state::action_states,\n};\n\nuse super::get_stack_and_server;\n\npub trait ExecuteCompose {\n  type Extras;\n\n  async fn execute(\n    periphery: PeripheryClient,\n    stack: Stack,\n    services: Vec<String>,\n    extras: Self::Extras,\n  ) -> anyhow::Result<Log>;\n}\n\npub async fn execute_compose<T: ExecuteCompose>(\n  stack: &str,\n  services: Vec<String>,\n  user: &User,\n  set_in_progress: impl Fn(&mut StackActionState),\n  mut update: Update,\n  extras: T::Extras,\n) -> anyhow::Result<Update> {\n  let (stack, server) = get_stack_and_server(\n    stack,\n    user,\n    PermissionLevel::Execute.into(),\n    true,\n  )\n  .await?;\n\n  // get the action state for the stack (or insert default).\n  let action_state =\n    action_states().stack.get_or_insert_default(&stack.id).await;\n\n  // Will check to ensure stack not already busy before updating, and return Err if so.\n  // The returned guard will set the action state back to default when dropped.\n  let _action_guard = action_state.update(set_in_progress)?;\n\n  // Send update here for frontend to recheck action state\n  update_update(update.clone()).await?;\n\n  let periphery = periphery_client(&server)?;\n\n  if !services.is_empty() {\n    update.logs.push(Log::simple(\n      \"Service/s\",\n      format!(\n        \"Execution requested for Stack service/s {}\",\n        services.join(\", \")\n      ),\n    ))\n  }\n\n  update\n    .logs\n    .push(T::execute(periphery, stack, services, extras).await?);\n\n  // Ensure cached stack state up to date by updating server cache\n  update_cache_for_server(&server, true).await;\n\n  update.finalize();\n  update_update(update.clone()).await?;\n\n  Ok(update)\n}\n\nfn service_args(services: &[String]) -> String {\n  if !services.is_empty() {\n    format!(\" {}\", services.join(\" \"))\n  } else {\n    String::new()\n  }\n}\n\nimpl ExecuteCompose for StartStack {\n  type Extras = ();\n  async fn execute(\n    periphery: PeripheryClient,\n    stack: Stack,\n    services: Vec<String>,\n    _: Self::Extras,\n  ) -> anyhow::Result<Log> {\n    let service_args = service_args(&services);\n    periphery\n      .request(ComposeExecution {\n        project: stack.project_name(false),\n        command: format!(\"start{service_args}\"),\n      })\n      .await\n  }\n}\n\nimpl ExecuteCompose for RestartStack {\n  type Extras = ();\n  async fn execute(\n    periphery: PeripheryClient,\n    stack: Stack,\n    services: Vec<String>,\n    _: Self::Extras,\n  ) -> anyhow::Result<Log> {\n    let service_args = service_args(&services);\n    periphery\n      .request(ComposeExecution {\n        project: stack.project_name(false),\n        command: format!(\"restart{service_args}\"),\n      })\n      .await\n  }\n}\n\nimpl ExecuteCompose for PauseStack {\n  type Extras = ();\n  async fn execute(\n    periphery: PeripheryClient,\n    stack: Stack,\n    services: Vec<String>,\n    _: Self::Extras,\n  ) -> anyhow::Result<Log> {\n    let service_args = service_args(&services);\n    periphery\n      .request(ComposeExecution {\n        project: stack.project_name(false),\n        command: format!(\"pause{service_args}\"),\n      })\n      .await\n  }\n}\n\nimpl ExecuteCompose for UnpauseStack {\n  type Extras = ();\n  async fn execute(\n    periphery: PeripheryClient,\n    stack: Stack,\n    services: Vec<String>,\n    _: Self::Extras,\n  ) -> anyhow::Result<Log> {\n    let service_args = service_args(&services);\n    periphery\n      .request(ComposeExecution {\n        project: stack.project_name(false),\n        command: format!(\"unpause{service_args}\"),\n      })\n      .await\n  }\n}\n\nimpl ExecuteCompose for StopStack {\n  type Extras = Option<i32>;\n  async fn execute(\n    periphery: PeripheryClient,\n    stack: Stack,\n    services: Vec<String>,\n    timeout: Self::Extras,\n  ) -> anyhow::Result<Log> {\n    let service_args = service_args(&services);\n    let maybe_timeout = maybe_timeout(timeout);\n    periphery\n      .request(ComposeExecution {\n        project: stack.project_name(false),\n        command: format!(\"stop{maybe_timeout}{service_args}\"),\n      })\n      .await\n  }\n}\n\nimpl ExecuteCompose for DestroyStack {\n  type Extras = (Option<i32>, bool);\n  async fn execute(\n    periphery: PeripheryClient,\n    stack: Stack,\n    services: Vec<String>,\n    (timeout, remove_orphans): Self::Extras,\n  ) -> anyhow::Result<Log> {\n    let service_args = service_args(&services);\n    let maybe_timeout = maybe_timeout(timeout);\n    let maybe_remove_orphans = if remove_orphans {\n      \" --remove-orphans\"\n    } else {\n      \"\"\n    };\n    periphery\n      .request(ComposeExecution {\n        project: stack.project_name(false),\n        command: format!(\n          \"down{maybe_timeout}{maybe_remove_orphans}{service_args}\"\n        ),\n      })\n      .await\n  }\n}\n\npub fn maybe_timeout(timeout: Option<i32>) -> String {\n  if let Some(timeout) = timeout {\n    format!(\" --timeout {timeout}\")\n  } else {\n    String::new()\n  }\n}\n"
  },
  {
    "path": "bin/core/src/stack/mod.rs",
    "content": "use anyhow::{Context, anyhow};\nuse komodo_client::entities::{\n  permission::PermissionLevelAndSpecifics,\n  server::{Server, ServerState},\n  stack::Stack,\n  user::User,\n};\nuse regex::Regex;\n\nuse crate::{\n  helpers::query::get_server_with_state,\n  permission::get_check_permissions,\n};\n\npub mod execute;\npub mod remote;\npub mod services;\n\npub async fn get_stack_and_server(\n  stack: &str,\n  user: &User,\n  permissions: PermissionLevelAndSpecifics,\n  block_if_server_unreachable: bool,\n) -> anyhow::Result<(Stack, Server)> {\n  let stack =\n    get_check_permissions::<Stack>(stack, user, permissions).await?;\n\n  if stack.config.server_id.is_empty() {\n    return Err(anyhow!(\"Stack has no server configured\"));\n  }\n\n  let (server, state) =\n    get_server_with_state(&stack.config.server_id).await?;\n  if block_if_server_unreachable && state != ServerState::Ok {\n    return Err(anyhow!(\n      \"Cannot send command when server is unreachable or disabled\"\n    ));\n  }\n\n  Ok((stack, server))\n}\n\npub fn compose_container_match_regex(\n  container_name: &str,\n) -> anyhow::Result<Regex> {\n  let regex = format!(\"^{container_name}-?[0-9]*$\");\n  Regex::new(&regex).with_context(|| {\n    format!(\"failed to construct valid regex from {regex}\")\n  })\n}\n"
  },
  {
    "path": "bin/core/src/stack/remote.rs",
    "content": "use std::{fs, path::PathBuf};\n\nuse anyhow::Context;\nuse formatting::format_serror;\nuse komodo_client::entities::{\n  FileContents, RepoExecutionArgs,\n  repo::Repo,\n  stack::{Stack, StackRemoteFileContents},\n  update::Log,\n};\n\nuse crate::{config::core_config, helpers::git_token};\n\npub struct RemoteComposeContents {\n  pub successful: Vec<StackRemoteFileContents>,\n  pub errored: Vec<FileContents>,\n  pub hash: Option<String>,\n  pub message: Option<String>,\n  pub _logs: Vec<Log>,\n}\n\n/// Returns Result<(read paths, error paths, logs, short hash, commit message)>\npub async fn get_repo_compose_contents(\n  stack: &Stack,\n  repo: Option<&Repo>,\n  // Collect any files which are missing in the repo.\n  mut missing_files: Option<&mut Vec<String>>,\n) -> anyhow::Result<RemoteComposeContents> {\n  let clone_args: RepoExecutionArgs =\n    repo.map(Into::into).unwrap_or(stack.into());\n  let (repo_path, _logs, hash, message) =\n    ensure_remote_repo(clone_args)\n      .await\n      .context(\"Failed to clone stack repo\")?;\n\n  let run_directory = repo_path.join(&stack.config.run_directory);\n  // This will remove any intermediate '/./' which can be a problem for some OS.\n  let run_directory = run_directory.components().collect::<PathBuf>();\n\n  let mut successful = Vec::new();\n  let mut errored = Vec::new();\n\n  for file in stack.all_file_dependencies() {\n    let file_path = run_directory.join(&file.path);\n    if !file_path.exists()\n      && let Some(missing_files) = &mut missing_files\n    {\n      missing_files.push(file.path.clone());\n    }\n    // If file does not exist, will show up in err case so the log is handled\n    match fs::read_to_string(&file_path).with_context(|| {\n      format!(\"Failed to read file contents from {file_path:?}\")\n    }) {\n      Ok(contents) => successful.push(StackRemoteFileContents {\n        path: file.path,\n        contents,\n        services: file.services,\n        requires: file.requires,\n      }),\n      Err(e) => errored.push(FileContents {\n        path: file.path,\n        contents: format_serror(&e.into()),\n      }),\n    }\n  }\n\n  Ok(RemoteComposeContents {\n    successful,\n    errored,\n    hash,\n    message,\n    _logs,\n  })\n}\n\n/// Returns (destination, logs, hash, message)\npub async fn ensure_remote_repo(\n  mut clone_args: RepoExecutionArgs,\n) -> anyhow::Result<(PathBuf, Vec<Log>, Option<String>, Option<String>)>\n{\n  let config = core_config();\n\n  let access_token = if let Some(username) = &clone_args.account {\n    git_token(&clone_args.provider, username, |https| {\n        clone_args.https = https\n      })\n      .await\n      .with_context(\n        || format!(\"Failed to get git token in call to db. Stopping run. | {} | {username}\", clone_args.provider),\n      )?\n  } else {\n    None\n  };\n\n  let repo_path =\n    clone_args.unique_path(&core_config().repo_directory)?;\n  clone_args.destination = Some(repo_path.display().to_string());\n\n  git::pull_or_clone(clone_args, &config.repo_directory, access_token)\n    .await\n    .context(\"Failed to clone stack repo\")\n    .map(|(res, _)| {\n      (repo_path, res.logs, res.commit_hash, res.commit_message)\n    })\n}\n"
  },
  {
    "path": "bin/core/src/stack/services.rs",
    "content": "use anyhow::Context;\nuse komodo_client::entities::stack::{\n  ComposeFile, ComposeService, ComposeServiceDeploy, Stack,\n  StackServiceNames,\n};\n\npub fn extract_services_from_stack(\n  stack: &Stack,\n) -> Vec<StackServiceNames> {\n  if let Some(mut services) = stack.info.deployed_services.clone() {\n    for service in services.iter_mut().filter(|s| s.image.is_empty())\n    {\n      service.image = stack\n        .info\n        .latest_services\n        .iter()\n        .find(|s| s.service_name == service.service_name)\n        .map(|s| s.image.clone())\n        .unwrap_or_default();\n    }\n    services\n  } else {\n    stack.info.latest_services.clone()\n  }\n}\n\npub fn extract_services_into_res(\n  project_name: &str,\n  compose_contents: &str,\n  res: &mut Vec<StackServiceNames>,\n) -> anyhow::Result<()> {\n  let compose =\n    serde_yaml_ng::from_str::<ComposeFile>(compose_contents)\n      .context(\n        \"failed to parse service names from compose contents\",\n      )?;\n\n  let mut services = Vec::with_capacity(compose.services.capacity());\n\n  for (\n    service_name,\n    ComposeService {\n      container_name,\n      deploy,\n      image,\n    },\n  ) in compose.services\n  {\n    let image = image.unwrap_or_default();\n    match deploy {\n      Some(ComposeServiceDeploy {\n        replicas: Some(replicas),\n      }) if replicas > 1 => {\n        for i in 1..1 + replicas {\n          services.push(StackServiceNames {\n            container_name: format!(\n              \"{project_name}-{service_name}-{i}\"\n            ),\n            service_name: format!(\"{service_name}-{i}\"),\n            image: image.clone(),\n          });\n        }\n      }\n      _ => {\n        services.push(StackServiceNames {\n          container_name: container_name.unwrap_or_else(|| {\n            format!(\"{project_name}-{service_name}\")\n          }),\n          service_name,\n          image,\n        });\n      }\n    }\n  }\n\n  res.extend(services);\n\n  Ok(())\n}\n"
  },
  {
    "path": "bin/core/src/startup.rs",
    "content": "use std::str::FromStr;\n\nuse colored::Colorize;\nuse database::mungos::{\n  find::find_collect,\n  mongodb::bson::{Document, doc, oid::ObjectId, to_document},\n};\nuse futures::future::join_all;\nuse komodo_client::{\n  api::{\n    auth::SignUpLocalUser,\n    execute::{\n      BackupCoreDatabase, Execution, GlobalAutoUpdate, RunAction,\n    },\n    write::{\n      CreateBuilder, CreateProcedure, CreateServer, CreateTag,\n      UpdateResourceMeta,\n    },\n  },\n  entities::{\n    ResourceTarget,\n    builder::{PartialBuilderConfig, PartialServerBuilderConfig},\n    komodo_timestamp,\n    procedure::{EnabledExecution, ProcedureConfig, ProcedureStage},\n    server::{PartialServerConfig, Server},\n    sync::ResourceSync,\n    tag::TagColor,\n    update::Log,\n    user::{action_user, system_user},\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  api::{\n    auth::AuthArgs,\n    execute::{ExecuteArgs, ExecuteRequest},\n    write::WriteArgs,\n  },\n  config::core_config,\n  helpers::update::init_execution_update,\n  network, resource,\n  state::db_client,\n};\n\n/// Runs the Actions with `run_at_startup: true`\npub async fn run_startup_actions() {\n  let startup_actions = match find_collect(\n    &db_client().actions,\n    doc! { \"config.run_at_startup\": true },\n    None,\n  )\n  .await\n  {\n    Ok(actions) => actions,\n    Err(e) => {\n      error!(\"Failed to fetch actions for startup | {e:#?}\");\n      return;\n    }\n  };\n\n  for action in startup_actions {\n    let name = action.name;\n    let id = action.id;\n    let update = match init_execution_update(\n      &ExecuteRequest::RunAction(RunAction {\n        action: name.clone(),\n        args: Default::default(),\n      }),\n      action_user(),\n    )\n    .await\n    {\n      Ok(update) => update,\n      Err(e) => {\n        error!(\n          \"Failed to initialize update for action {name} ({id}) | {e:#?}\"\n        );\n        continue;\n      }\n    };\n\n    if let Err(e) = (RunAction {\n      action: name.clone(),\n      args: Default::default(),\n    })\n    .resolve(&ExecuteArgs {\n      user: action_user().to_owned(),\n      update,\n    })\n    .await\n    {\n      error!(\n        \"Failed to execute startup action {name} ({id}) | {e:#?}\"\n      );\n    }\n  }\n}\n\n/// This function should be run on startup,\n/// after the db client has been initialized\npub async fn on_startup() {\n  // Configure manual network interface if specified\n  network::configure_internet_gateway().await;\n\n  tokio::join!(\n    in_progress_update_cleanup(),\n    open_alert_cleanup(),\n    clean_up_server_templates(),\n    ensure_first_server_and_builder(),\n    ensure_init_user_and_resources(),\n  );\n}\n\nasync fn in_progress_update_cleanup() {\n  let log = Log::error(\n    \"Komodo shutdown\",\n    String::from(\n      \"Komodo shutdown during execution. If this is a build, the builder may not have been terminated.\",\n    ),\n  );\n  // This static log won't fail to serialize, unwrap ok.\n  let log = to_document(&log).unwrap();\n  if let Err(e) = db_client()\n    .updates\n    .update_many(\n      doc! { \"status\": \"InProgress\" },\n      doc! {\n        \"$set\": {\n          \"status\": \"Complete\",\n          \"success\": false,\n        },\n        \"$push\": {\n          \"logs\": log\n        }\n      },\n    )\n    .await\n  {\n    error!(\"failed to cleanup in progress updates on startup | {e:#}\")\n  }\n}\n\n/// Run on startup, ensure open alerts pointing to invalid resources are closed.\nasync fn open_alert_cleanup() {\n  let db = db_client();\n  let Ok(alerts) =\n    find_collect(&db.alerts, doc! { \"resolved\": false }, None)\n      .await\n      .inspect_err(|e| {\n        error!(\n          \"failed to list all alerts for startup open alert cleanup | {e:?}\"\n        )\n      })\n  else {\n    return;\n  };\n  let futures = alerts.into_iter().map(|alert| async move {\n    match alert.target {\n      ResourceTarget::Server(id) => {\n        resource::get::<Server>(&id)\n          .await\n          .is_err()\n          .then(|| ObjectId::from_str(&alert.id).inspect_err(|e| warn!(\"failed to clean up alert - id is invalid ObjectId | {e:?}\")).ok()).flatten()\n      }\n      ResourceTarget::ResourceSync(id) => {\n        resource::get::<ResourceSync>(&id)\n          .await\n          .is_err()\n          .then(|| ObjectId::from_str(&alert.id).inspect_err(|e| warn!(\"failed to clean up alert - id is invalid ObjectId | {e:?}\")).ok()).flatten()\n      }\n      // No other resources should have open alerts.\n      _ => ObjectId::from_str(&alert.id).inspect_err(|e| warn!(\"failed to clean up alert - id is invalid ObjectId | {e:?}\")).ok(),\n    }\n  });\n  let to_update_ids = join_all(futures)\n    .await\n    .into_iter()\n    .flatten()\n    .collect::<Vec<_>>();\n  if let Err(e) = db\n    .alerts\n    .update_many(\n      doc! { \"_id\": { \"$in\": to_update_ids } },\n      doc! { \"$set\": {\n        \"resolved\": true,\n        \"resolved_ts\": komodo_timestamp()\n      } },\n    )\n    .await\n  {\n    error!(\n      \"failed to clean up invalid open alerts on startup | {e:#}\"\n    )\n  }\n}\n\n/// Ensures a default server / builder exists with the defined address\nasync fn ensure_first_server_and_builder() {\n  let config = core_config();\n  let Some(address) = config.first_server.clone() else {\n    return;\n  };\n  let db = db_client();\n  let Ok(server) = db\n    .servers\n    .find_one(Document::new())\n    .await\n    .inspect_err(|e| error!(\"Failed to initialize 'first_server'. Failed to query db. {e:?}\"))\n  else {\n    return;\n  };\n  let server = if let Some(server) = server {\n    server\n  } else {\n    match (CreateServer {\n      name: config.first_server_name.clone(),\n      config: PartialServerConfig {\n        address: Some(address),\n        enabled: Some(true),\n        ..Default::default()\n      },\n    })\n    .resolve(&WriteArgs {\n      user: system_user().to_owned(),\n    })\n    .await\n    {\n      Ok(server) => server,\n      Err(e) => {\n        error!(\n          \"Failed to initialize 'first_server'. Failed to CreateServer. {:#}\",\n          e.error\n        );\n        return;\n      }\n    }\n  };\n  let Ok(None) = db.builders\n    .find_one(Document::new()).await\n    .inspect_err(|e| error!(\"Failed to initialize 'first_builder' | Failed to query db | {e:?}\")) else {\n      return;\n    };\n  if let Err(e) = (CreateBuilder {\n    name: config.first_server_name.clone(),\n    config: PartialBuilderConfig::Server(\n      PartialServerBuilderConfig {\n        server_id: Some(server.id),\n      },\n    ),\n  })\n  .resolve(&WriteArgs {\n    user: system_user().to_owned(),\n  })\n  .await\n  {\n    error!(\n      \"Failed to initialize 'first_builder' | Failed to CreateBuilder | {:#}\",\n      e.error\n    );\n  }\n}\n\nasync fn ensure_init_user_and_resources() {\n  let db = db_client();\n\n  // Assumes if there are any existing users, procedures, or tags,\n  // the default procedures do not need to be set up.\n  let Ok((None, None, None)) = tokio::try_join!(\n    db.users.find_one(Document::new()),\n    db.procedures.find_one(Document::new()),\n    db.tags.find_one(Document::new()),\n  ).inspect_err(|e| error!(\"Failed to initialize default procedures | Failed to query db | {e:?}\")) else {\n    return\n  };\n\n  let config = core_config();\n\n  // Init admin user if set in config.\n  if let Some(username) = &config.init_admin_username {\n    info!(\"Creating init admin user...\");\n    SignUpLocalUser {\n      username: username.clone(),\n      password: config.init_admin_password.clone(),\n    }\n    .resolve(&AuthArgs::default())\n    .await\n    .expect(\"Failed to initialize default admin user.\");\n    db.users\n      .find_one(doc! { \"username\": username })\n      .await\n      .expect(\"Failed to query database for initial user\")\n      .expect(\"Failed to find initial user after creation\");\n  };\n\n  if config.disable_init_resources {\n    info!(\"System resources init {}\", \"DISABLED\".red());\n    return;\n  }\n\n  info!(\"Creating init system resources...\");\n\n  let write_args = WriteArgs {\n    user: system_user().to_owned(),\n  };\n\n  // Create default 'system' tag\n  let default_tags = match (CreateTag {\n    name: String::from(\"system\"),\n    color: Some(TagColor::Red),\n  })\n  .resolve(&write_args)\n  .await\n  {\n    Ok(tag) => vec![tag.id],\n    Err(e) => {\n      warn!(\"Failed to create default tag | {:#}\", e.error);\n      Vec::new()\n    }\n  };\n\n  // Backup Core Database\n  async {\n    let Ok(config) = ProcedureConfig::builder()\n      .stages(vec![ProcedureStage {\n        name: String::from(\"Stage 1\"),\n        enabled: true,\n        executions: vec![\n          EnabledExecution {\n            execution: Execution::BackupCoreDatabase(BackupCoreDatabase {}),\n            enabled: true\n          }\n        ]\n      }])\n      .schedule(String::from(\"Every day at 01:00\"))\n      .build()\n      .inspect_err(|e| error!(\"Failed to initialize backup core database procedure | Failed to build Procedure | {e:?}\")) else {\n      return;\n    };\n    let procedure = match (CreateProcedure {\n      name: String::from(\"Backup Core Database\"),\n      config: config.into()\n    }).resolve(&write_args).await {\n      Ok(procedure) => procedure,\n      Err(e) => {\n        error!(\n          \"Failed to initialize default database backup Procedure | Failed to create Procedure | {:#}\",\n          e.error\n        );\n        return;\n      }\n    };\n    if let Err(e) = (UpdateResourceMeta {\n      target: ResourceTarget::Procedure(procedure.id),\n      tags: Some(default_tags.clone()),\n      description: Some(String::from(\n        \"Triggers the Core database backup at the scheduled time.\",\n      )),\n      template: None,\n    }).resolve(&write_args).await {\n      warn!(\"Failed to update default database backup Procedure tags / description | {:#}\", e.error);\n    }\n  }.await;\n\n  // GlobalAutoUpdate\n  async {\n    let Ok(config) = ProcedureConfig::builder()\n      .stages(vec![ProcedureStage {\n        name: String::from(\"Stage 1\"),\n        enabled: true,\n        executions: vec![\n          EnabledExecution {\n            execution: Execution::GlobalAutoUpdate(GlobalAutoUpdate {}),\n            enabled: true\n          }\n        ]\n      }])\n      .schedule(String::from(\"Every day at 03:00\"))\n      .build()\n      .inspect_err(|e| error!(\"Failed to initialize global auto update procedure | Failed to build Procedure | {e:?}\")) else {\n      return;\n    };\n    let procedure = match (CreateProcedure {\n      name: String::from(\"Global Auto Update\"),\n      config: config.into(),\n    })\n    .resolve(&write_args)\n    .await\n    {\n      Ok(procedure) => procedure,\n      Err(e) => {\n        error!(\n          \"Failed to initialize global auto update Procedure | Failed to create Procedure | {:#}\",\n          e.error\n        );\n        return;\n      }\n    };\n    if let Err(e) = (UpdateResourceMeta {\n      target: ResourceTarget::Procedure(procedure.id),\n      tags: Some(default_tags.clone()),\n      description: Some(String::from(\n        \"Pulls and auto updates Stacks and Deployments using 'poll_for_updates' or 'auto_update'.\",\n      )),\n      template: None,\n    })\n    .resolve(&write_args)\n    .await\n    {\n      warn!(\n        \"Failed to update global auto update Procedure tags / description | {:#}\",\n        e.error\n      );\n    }\n  }.await;\n}\n\n/// v1.17.5 removes the ServerTemplate resource.\n/// References to this resource type need to be cleaned up\n/// to avoid type errors reading from the database.\nasync fn clean_up_server_templates() {\n  let db = db_client();\n  tokio::join!(\n    async {\n      db.permissions\n        .delete_many(doc! {\n          \"resource_target.type\": \"ServerTemplate\",\n        })\n        .await\n        .expect(\n          \"Failed to clean up server template permissions on db\",\n        );\n    },\n    async {\n      db.updates\n        .delete_many(doc! { \"target.type\": \"ServerTemplate\" })\n        .await\n        .expect(\"Failed to clean up server template updates on db\");\n    },\n    async {\n      db.users\n        .update_many(\n          Document::new(),\n          doc! { \"$unset\": { \"recents.ServerTemplate\": 1, \"all.ServerTemplate\": 1 } }\n        )\n        .await\n        .expect(\"Failed to clean up server template updates on db\");\n    },\n    async {\n      db.user_groups\n        .update_many(\n          Document::new(),\n          doc! { \"$unset\": { \"all.ServerTemplate\": 1 } },\n        )\n        .await\n        .expect(\"Failed to clean up server template updates on db\");\n    },\n  );\n}\n"
  },
  {
    "path": "bin/core/src/state.rs",
    "content": "use std::{\n  collections::HashMap,\n  sync::{Arc, OnceLock},\n};\n\nuse anyhow::Context;\nuse arc_swap::ArcSwap;\nuse database::Client;\nuse komodo_client::entities::{\n  action::ActionState,\n  build::BuildState,\n  config::core::{CoreConfig, GithubWebhookAppConfig},\n  deployment::DeploymentState,\n  procedure::ProcedureState,\n  repo::RepoState,\n  stack::StackState,\n};\nuse octorust::auth::{\n  Credentials, InstallationTokenGenerator, JWTCredentials,\n};\n\nuse crate::{\n  auth::jwt::JwtClient,\n  config::core_config,\n  helpers::{\n    action_state::ActionStates, all_resources::AllResourcesById,\n    cache::Cache,\n  },\n  monitor::{\n    CachedDeploymentStatus, CachedRepoStatus, CachedServerStatus,\n    CachedStackStatus, History,\n  },\n};\n\nstatic DB_CLIENT: OnceLock<Client> = OnceLock::new();\n\npub fn db_client() -> &'static Client {\n  DB_CLIENT\n    .get()\n    .expect(\"db_client accessed before initialized\")\n}\n\n/// Must be called in app startup sequence.\npub async fn init_db_client() {\n  let client = Client::new(&core_config().database)\n    .await\n    .context(\"failed to initialize database client\")\n    .unwrap();\n  DB_CLIENT.set(client).expect(\"db_clint\");\n}\n\npub fn jwt_client() -> &'static JwtClient {\n  static JWT_CLIENT: OnceLock<JwtClient> = OnceLock::new();\n  JWT_CLIENT.get_or_init(|| match JwtClient::new(core_config()) {\n    Ok(client) => client,\n    Err(e) => {\n      error!(\"failed to initialialize JwtClient | {e:#}\");\n      panic!(\"Exiting\");\n    }\n  })\n}\n\npub fn github_client()\n-> Option<&'static HashMap<String, octorust::Client>> {\n  static GITHUB_CLIENT: OnceLock<\n    Option<HashMap<String, octorust::Client>>,\n  > = OnceLock::new();\n  GITHUB_CLIENT\n    .get_or_init(|| {\n      let CoreConfig {\n        github_webhook_app:\n          GithubWebhookAppConfig {\n            app_id,\n            installations,\n            pk_path,\n            ..\n          },\n        ..\n      } = core_config();\n      if *app_id == 0 || installations.is_empty() {\n        return None;\n      }\n      let private_key = match std::fs::read(pk_path).with_context(|| format!(\"github webhook app | failed to load private key at {pk_path}\")) {\n        Ok(key) => key,\n        Err(e) => {\n          error!(\"{e:#}\");\n          return None;\n        }\n      };\n\n      let private_key = match nom_pem::decode_block(&private_key) {\n        Ok(key) => key,\n        Err(e) => {\n          error!(\"github webhook app | failed to decode private key at {pk_path} | {e:?}\");\n          return None;\n        }\n      };\n\n      let jwt = match JWTCredentials::new(*app_id, private_key.data).context(\"failed to initialize github JWTCredentials\") {\n        Ok(jwt) => jwt,\n        Err(e) => {\n          error!(\"github webhook app | failed to make github JWTCredentials | pk path: {pk_path} | {e:#}\");\n          return None\n        }\n      };\n\n      let mut clients =\n        HashMap::with_capacity(installations.capacity());\n\n      for installation in installations {\n        let token_generator = InstallationTokenGenerator::new(\n          installation.id,\n          jwt.clone(),\n        );\n        let client = match octorust::Client::new(\n          \"github-app\",\n          Credentials::InstallationToken(token_generator),\n        ).with_context(|| format!(\"failed to initialize github webhook client for installation {}\", installation.id)) {\n          Ok(client) => client,\n          Err(e) => {\n            error!(\"{e:#}\");\n            continue;\n          }\n        };\n        clients.insert(installation.namespace.to_string(), client);\n      }\n\n      Some(clients)\n    })\n    .as_ref()\n}\n\npub fn action_states() -> &'static ActionStates {\n  static ACTION_STATES: OnceLock<ActionStates> = OnceLock::new();\n  ACTION_STATES.get_or_init(ActionStates::default)\n}\n\n/// Cache of ids to status\npub type DeploymentStatusCache = Cache<\n  String,\n  Arc<History<CachedDeploymentStatus, DeploymentState>>,\n>;\n\n/// Cache of ids to status\npub fn deployment_status_cache() -> &'static DeploymentStatusCache {\n  static DEPLOYMENT_STATUS_CACHE: OnceLock<DeploymentStatusCache> =\n    OnceLock::new();\n  DEPLOYMENT_STATUS_CACHE.get_or_init(Default::default)\n}\n\npub type StackStatusCache =\n  Cache<String, Arc<History<CachedStackStatus, StackState>>>;\n\npub fn stack_status_cache() -> &'static StackStatusCache {\n  static STACK_STATUS_CACHE: OnceLock<StackStatusCache> =\n    OnceLock::new();\n  STACK_STATUS_CACHE.get_or_init(Default::default)\n}\n\npub type ServerStatusCache = Cache<String, Arc<CachedServerStatus>>;\n\npub fn server_status_cache() -> &'static ServerStatusCache {\n  static SERVER_STATUS_CACHE: OnceLock<ServerStatusCache> =\n    OnceLock::new();\n  SERVER_STATUS_CACHE.get_or_init(Default::default)\n}\n\npub type RepoStatusCache = Cache<String, Arc<CachedRepoStatus>>;\n\npub fn repo_status_cache() -> &'static RepoStatusCache {\n  static REPO_STATUS_CACHE: OnceLock<RepoStatusCache> =\n    OnceLock::new();\n  REPO_STATUS_CACHE.get_or_init(Default::default)\n}\n\npub type BuildStateCache = Cache<String, BuildState>;\n\npub fn build_state_cache() -> &'static BuildStateCache {\n  static BUILD_STATE_CACHE: OnceLock<BuildStateCache> =\n    OnceLock::new();\n  BUILD_STATE_CACHE.get_or_init(Default::default)\n}\n\npub type RepoStateCache = Cache<String, RepoState>;\n\npub fn repo_state_cache() -> &'static RepoStateCache {\n  static REPO_STATE_CACHE: OnceLock<RepoStateCache> = OnceLock::new();\n  REPO_STATE_CACHE.get_or_init(Default::default)\n}\n\npub type ProcedureStateCache = Cache<String, ProcedureState>;\n\npub fn procedure_state_cache() -> &'static ProcedureStateCache {\n  static PROCEDURE_STATE_CACHE: OnceLock<ProcedureStateCache> =\n    OnceLock::new();\n  PROCEDURE_STATE_CACHE.get_or_init(Default::default)\n}\n\npub type ActionStateCache = Cache<String, ActionState>;\n\npub fn action_state_cache() -> &'static ActionStateCache {\n  static ACTION_STATE_CACHE: OnceLock<ActionStateCache> =\n    OnceLock::new();\n  ACTION_STATE_CACHE.get_or_init(Default::default)\n}\n\npub fn all_resources_cache() -> &'static ArcSwap<AllResourcesById> {\n  static ALL_RESOURCES: OnceLock<ArcSwap<AllResourcesById>> =\n    OnceLock::new();\n  ALL_RESOURCES.get_or_init(Default::default)\n}\n"
  },
  {
    "path": "bin/core/src/sync/deploy.rs",
    "content": "use std::{collections::HashMap, time::Duration};\n\nuse anyhow::{Context, anyhow};\nuse formatting::{Color, bold, colored, format_serror, muted};\nuse futures::future::join_all;\nuse komodo_client::{\n  api::{\n    execute::{Deploy, DeployStack},\n    read::ListBuildVersions,\n  },\n  entities::{\n    ResourceTarget,\n    deployment::{\n      Deployment, DeploymentConfig, DeploymentImage, DeploymentState,\n      PartialDeploymentConfig,\n    },\n    stack::{\n      PartialStackConfig, Stack, StackConfig,\n      StackRemoteFileContents, StackState,\n    },\n    sync::SyncDeployUpdate,\n    toml::ResourceToml,\n    update::Log,\n    user::sync_user,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{\n  api::{\n    execute::{ExecuteArgs, ExecuteRequest},\n    read::ReadArgs,\n  },\n  helpers::update::init_execution_update,\n  state::{deployment_status_cache, stack_status_cache},\n};\n\nuse super::ResourceSyncTrait;\n\n/// All entries in here are due to be deployed,\n/// after the given dependencies,\n/// with the given reason.\npub type ToDeployCache =\n  Vec<(ResourceTarget, String, Vec<ResourceTarget>)>;\n\n#[derive(Clone, Copy)]\npub struct SyncDeployParams<'a> {\n  pub deployments: &'a [ResourceToml<PartialDeploymentConfig>],\n  // Names to deployments\n  pub deployment_map: &'a HashMap<String, Deployment>,\n  pub stacks: &'a [ResourceToml<PartialStackConfig>],\n  // Names to stacks\n  pub stack_map: &'a HashMap<String, Stack>,\n}\n\npub async fn deploy_from_cache(\n  mut to_deploy: ToDeployCache,\n  logs: &mut Vec<Log>,\n) {\n  if to_deploy.is_empty() {\n    return;\n  }\n  let mut log = format!(\n    \"{}: running executions to sync deployment / stack state\",\n    muted(\"INFO\")\n  );\n  let mut round = 1;\n  let user = sync_user();\n\n  while !to_deploy.is_empty() {\n    // Collect all waiting deployments without waiting dependencies.\n    let good_to_deploy = to_deploy\n      .iter()\n      .filter(|(_, _, after)| {\n        to_deploy\n          .iter()\n          .all(|(target, _, _)| !after.contains(target))\n      })\n      // The target / reason need the be cloned out to to_deploy is not borrowed from.\n      // to_deploy will be mutably accessed later.\n      .map(|(target, reason, _)| (target.clone(), reason.clone()))\n      .collect::<HashMap<_, _>>();\n\n    // Deploy the ones ready for deployment\n    let res = join_all(good_to_deploy.iter().map(\n      |(target, reason)| async move {\n        let res = async {\n          match &target {\n            ResourceTarget::Deployment(name) => {\n              let req = ExecuteRequest::Deploy(Deploy {\n                deployment: name.to_string(),\n                stop_signal: None,\n                stop_time: None,\n              });\n\n              let update = init_execution_update(&req, user).await?;\n              let ExecuteRequest::Deploy(req) = req else {\n                unreachable!()\n              };\n              req\n                .resolve(&ExecuteArgs {\n                  user: user.to_owned(),\n                  update,\n                })\n                .await\n            }\n            ResourceTarget::Stack(name) => {\n              let req = ExecuteRequest::DeployStack(DeployStack {\n                stack: name.to_string(),\n                services: Vec::new(),\n                stop_time: None,\n              });\n\n              let update = init_execution_update(&req, user).await?;\n              let ExecuteRequest::DeployStack(req) = req else {\n                unreachable!()\n              };\n              req\n                .resolve(&ExecuteArgs {\n                  user: user.to_owned(),\n                  update,\n                })\n                .await\n            }\n            _ => unreachable!(),\n          }\n        }\n        .await;\n        (target, reason, res)\n      },\n    ))\n    .await;\n\n    let mut has_error = false;\n\n    // Log results of deploy\n    for (target, reason, res) in res {\n      let (resource, name) = target.extract_variant_id();\n      if let Err(e) = res {\n        has_error = true;\n        log.push_str(&format!(\n          \"\\n{}: failed to deploy {resource} '{}' in round {} | {:#}\",\n          colored(\"ERROR\", Color::Red),\n          bold(name),\n          bold(round),\n          e.error\n        ));\n      } else {\n        log.push_str(&format!(\n          \"\\n{}: deployed {resource} '{}' in round {} with reason: {reason}\",\n          muted(\"INFO\"),\n          bold(name),\n          bold(round)\n        ));\n      }\n    }\n\n    // Early exit if any deploy has errors\n    if has_error {\n      log.push_str(&format!(\n        \"\\n{}: exited in round {} {}\",\n        muted(\"INFO\"),\n        bold(round),\n        colored(\"with errors\", Color::Red)\n      ));\n      logs.push(Log::error(\"Sync Deploy\", log));\n      return;\n    }\n\n    // Remove the deployed ones from 'to_deploy'\n    to_deploy\n      .retain(|(target, _, _)| !good_to_deploy.contains_key(target));\n\n    // If there must be another round, these are dependent on the first round.\n    // Sleep for 1s to allow for first round to startup\n    if !to_deploy.is_empty() {\n      // Increment the round\n      round += 1;\n      tokio::time::sleep(Duration::from_secs(1)).await;\n    }\n  }\n\n  log.push_str(&format!(\n    \"\\n{}: finished after {} round{}\",\n    muted(\"INFO\"),\n    bold(round),\n    if round > 1 { \"s\" } else { Default::default() }\n  ));\n\n  logs.push(Log::simple(\"Sync Deploy\", log));\n}\n\npub async fn get_updates_for_view(\n  params: SyncDeployParams<'_>,\n) -> SyncDeployUpdate {\n  let inner = async {\n    let mut update = SyncDeployUpdate {\n      to_deploy: 0,\n      log: String::from(\"Deploy Updates\\n-------------------\\n\"),\n    };\n    let mut lines = Vec::<String>::new();\n    for (target, reason, after) in build_deploy_cache(params).await? {\n      update.to_deploy += 1;\n      let mut line = format!(\n        \"{}: {}. reason: {reason}\",\n        colored(\"Deploy\", Color::Green),\n        bold(format!(\"{target:?}\")),\n      );\n      if !after.is_empty() {\n        line.push_str(&format!(\n          \"\\n{}: {}\",\n          colored(\"After\", Color::Blue),\n          after\n            .iter()\n            .map(|target| format!(\"{target:?}\"))\n            .collect::<Vec<_>>()\n            .join(\", \")\n        ))\n      }\n      lines.push(line);\n    }\n\n    update.log.push_str(&lines.join(\"\\n-------------------\\n\"));\n\n    anyhow::Ok(update)\n  };\n  match inner.await {\n    Ok(res) => res,\n    Err(e) => SyncDeployUpdate {\n      to_deploy: 0,\n      log: format_serror(\n        &e.context(\"failed to get deploy updates for view\").into(),\n      ),\n    },\n  }\n}\n\n/// Entries are keyed by ResourceTargets wrapping \"name\" instead of \"id\".\n/// If entry is None, it is confirmed no-deploy.\n/// If it is Some, it is confirmed deploy with provided reason and dependencies.\n///\n/// Used to build up resources to deploy earlier in the sync.\ntype ToDeployCacheInner =\n  HashMap<ResourceTarget, Option<(String, Vec<ResourceTarget>)>>;\n\n/// Maps build ids to latest versions as string.\ntype BuildVersionCache = HashMap<String, String>;\n\npub async fn build_deploy_cache(\n  params: SyncDeployParams<'_>,\n) -> anyhow::Result<ToDeployCache> {\n  let mut cache = ToDeployCacheInner::new();\n  let mut build_version_cache = BuildVersionCache::new();\n\n  // Just ensure they are all in the cache by looping through them all\n  for deployment in params.deployments {\n    build_cache_for_deployment(\n      deployment,\n      params,\n      &mut cache,\n      &mut build_version_cache,\n    )\n    .await?;\n  }\n  for stack in params.stacks {\n    build_cache_for_stack(\n      stack,\n      params,\n      &mut cache,\n      &mut build_version_cache,\n    )\n    .await?;\n  }\n\n  let cache = cache\n    .into_iter()\n    .filter_map(|(target, entry)| {\n      let (reason, after) = entry?;\n      Some((target, (reason, after)))\n    })\n    .collect::<HashMap<_, _>>();\n\n  // Have to clone here to use it after 'into_iter' below.\n  // All entries in cache at this point are deploying.\n  let clone = cache.clone();\n\n  Ok(\n    cache\n      .into_iter()\n      .map(|(target, (reason, mut after))| {\n        // Only keep targets which are deploying.\n        after.retain(|target| clone.contains_key(target));\n        (target, reason, after)\n      })\n      .collect(),\n  )\n}\n\ntype BuildRes<'a> = std::pin::Pin<\n  Box<\n    dyn std::future::Future<Output = anyhow::Result<()>> + Send + 'a,\n  >,\n>;\n\nfn build_cache_for_deployment<'a>(\n  deployment: &'a ResourceToml<PartialDeploymentConfig>,\n  SyncDeployParams {\n    deployments,\n    deployment_map,\n    stacks,\n    stack_map,\n  }: SyncDeployParams<'a>,\n  cache: &'a mut ToDeployCacheInner,\n  build_version_cache: &'a mut BuildVersionCache,\n) -> BuildRes<'a> {\n  Box::pin(async move {\n    let target = ResourceTarget::Deployment(deployment.name.clone());\n\n    // First check existing, and continue if already handled.\n    if cache.contains_key(&target) {\n      return Ok(());\n    }\n\n    // Check if deployment doesn't have \"deploy\" enabled.\n    if !deployment.deploy {\n      cache.insert(target, None);\n      return Ok(());\n    }\n\n    let after = get_after_as_resource_targets(\n      &deployment.name,\n      &deployment.after,\n      deployment_map,\n      deployments,\n      stack_map,\n      stacks,\n    )?;\n\n    let Some(original) = deployment_map.get(&deployment.name) else {\n      // This block is the None case, deployment is not created, should definitely deploy\n      cache.insert(\n        target,\n        Some((String::from(\"deploy on creation\"), after)),\n      );\n      return Ok(());\n    };\n\n    let status = &deployment_status_cache()\n      .get_or_insert_default(&original.id)\n      .await\n      .curr;\n    let state = status.state;\n\n    match state {\n      DeploymentState::Unknown => {\n        // Can't do anything with unknown state\n        cache.insert(target, None);\n        return Ok(());\n      }\n      DeploymentState::Running => {\n        // Here can diff the changes, to see if they merit a redeploy.\n\n        // First merge toml resource config (partial) onto default resource config.\n        // Makes sure things that aren't defined in toml (come through as None) actually get removed.\n        let config: DeploymentConfig =\n          deployment.config.clone().into();\n        let mut config: PartialDeploymentConfig = config.into();\n\n        Deployment::validate_partial_config(&mut config);\n\n        let mut diff =\n          Deployment::get_diff(original.config.clone(), config)?;\n\n        Deployment::validate_diff(&mut diff);\n        // Needs to only check config fields that affect docker run\n        let changed = diff.server_id.is_some()\n          || diff.image.is_some()\n          || diff.image_registry_account.is_some()\n          || diff.skip_secret_interp.is_some()\n          || diff.network.is_some()\n          || diff.restart.is_some()\n          || diff.command.is_some()\n          || diff.extra_args.is_some()\n          || diff.ports.is_some()\n          || diff.volumes.is_some()\n          || diff.environment.is_some()\n          || diff.labels.is_some();\n        if changed {\n          cache.insert(\n            target,\n            Some((\n              String::from(\"deployment config has changed\"),\n              after,\n            )),\n          );\n          return Ok(());\n        }\n      }\n      // All other cases will require Deploy to enter Running state.\n      _ => {\n        cache.insert(\n          target,\n          Some((\n            format!(\n              \"deployment has {} state\",\n              colored(state, Color::Red)\n            ),\n            after,\n          )),\n        );\n        return Ok(());\n      }\n    };\n\n    // We know the config hasn't changed at this point, but still need\n    // to check if attached build has updated. Can check original for this (know it hasn't changed)\n    if let DeploymentImage::Build { build_id, version } =\n      &original.config.image\n    {\n      // check if version is none, ie use latest build\n      if !version.is_none() {\n        let deployed_version = status\n          .container\n          .as_ref()\n          .and_then(|c| c.image.as_ref()?.split(':').next_back())\n          .unwrap_or(\"0.0.0\");\n        match build_version_cache.get(build_id) {\n          Some(version) if deployed_version != version => {\n            cache.insert(\n              target,\n              Some((\n                format!(\"build has new version: {version}\"),\n                after,\n              )),\n            );\n            return Ok(());\n          }\n          // Build version is the same, still need to check 'after'\n          Some(_) => {}\n          None => {\n            let Some(version) = (ListBuildVersions {\n              build: build_id.to_string(),\n              limit: Some(1),\n              ..Default::default()\n            })\n            .resolve(&ReadArgs {\n              user: sync_user().to_owned(),\n            })\n            .await\n            .map_err(|e| e.error)\n            .context(\"failed to get build versions\")?\n            .pop() else {\n              // The build has never been built.\n              // Skip deploy regardless of 'after' (it can't be deployed)\n              // Not sure how this would be reached on Running deployment...\n              cache.insert(target, None);\n              return Ok(());\n            };\n            let version = version.version.to_string();\n            build_version_cache\n              .insert(build_id.to_string(), version.clone());\n            if deployed_version != version {\n              // Same as 'Some' case out of the cache\n              cache.insert(\n                target,\n                Some((\n                  format!(\"build has new version: {version}\"),\n                  after,\n                )),\n              );\n              return Ok(());\n            }\n          }\n        }\n      }\n    };\n\n    // Check 'after' to see if they deploy.\n    insert_target_using_after_list(\n      target,\n      after,\n      SyncDeployParams {\n        deployments,\n        deployment_map,\n        stacks,\n        stack_map,\n      },\n      cache,\n      build_version_cache,\n    )\n    .await\n  })\n}\n\nfn build_cache_for_stack<'a>(\n  stack: &'a ResourceToml<PartialStackConfig>,\n  SyncDeployParams {\n    deployments,\n    deployment_map,\n    stacks,\n    stack_map,\n  }: SyncDeployParams<'a>,\n  cache: &'a mut ToDeployCacheInner,\n  build_version_cache: &'a mut BuildVersionCache,\n) -> BuildRes<'a> {\n  Box::pin(async move {\n    let target = ResourceTarget::Stack(stack.name.clone());\n\n    // First check existing, and continue if already handled.\n    if cache.contains_key(&target) {\n      return Ok(());\n    }\n\n    // Check if stack doesn't have \"deploy\" enabled.\n    if !stack.deploy {\n      cache.insert(target, None);\n      return Ok(());\n    }\n\n    let after = get_after_as_resource_targets(\n      &stack.name,\n      &stack.after,\n      deployment_map,\n      deployments,\n      stack_map,\n      stacks,\n    )?;\n\n    let Some(original) = stack_map.get(&stack.name) else {\n      // This block is the None case, deployment is not created, should definitely deploy\n      cache.insert(\n        target,\n        Some((String::from(\"deploy on creation\"), after)),\n      );\n      return Ok(());\n    };\n\n    let status = &stack_status_cache()\n      .get_or_insert_default(&original.id)\n      .await\n      .curr;\n    let state = status.state;\n\n    match state {\n      StackState::Unknown => {\n        // Can't do anything with unknown state\n        cache.insert(target, None);\n        return Ok(());\n      }\n      StackState::Running => {\n        // Here can diff the changes, to see if they merit a redeploy.\n\n        // See if any remote contents don't match deployed contents\n        #[allow(clippy::single_match)]\n        match (\n          &original.info.deployed_contents,\n          &original.info.remote_contents,\n        ) {\n          (Some(deployed_contents), Some(remote_contents)) => {\n            for StackRemoteFileContents {\n              path,\n              contents,\n              services: _services,\n              requires: _requires,\n            } in remote_contents\n            {\n              if let Some(deployed) =\n                deployed_contents.iter().find(|c| &c.path == path)\n              {\n                if &deployed.contents != contents {\n                  cache.insert(\n                    target,\n                    Some((\n                      format!(\n                        \"File contents for {path} have changed\"\n                      ),\n                      after,\n                    )),\n                  );\n                  return Ok(());\n                }\n              } else {\n                cache.insert(\n                  target,\n                  Some((\n                    format!(\"New file contents at {path}\"),\n                    after,\n                  )),\n                );\n                return Ok(());\n              }\n            }\n          }\n          // Maybe should handle other cases\n          _ => {}\n        }\n\n        // Merge toml resource config (partial) onto default resource config.\n        // Makes sure things that aren't defined in toml (come through as None) actually get removed.\n        let config: StackConfig = stack.config.clone().into();\n        let mut config: PartialStackConfig = config.into();\n\n        Stack::validate_partial_config(&mut config);\n\n        let mut diff =\n          Stack::get_diff(original.config.clone(), config)?;\n\n        Stack::validate_diff(&mut diff);\n        // Needs to only check config fields that affect docker compose command\n        let changed = diff.server_id.is_some()\n          || diff.project_name.is_some()\n          || diff.run_directory.is_some()\n          || diff.file_paths.is_some()\n          || diff.file_contents.is_some()\n          || diff.skip_secret_interp.is_some()\n          || diff.extra_args.is_some()\n          || diff.environment.is_some()\n          || diff.env_file_path.is_some()\n          || diff.repo.is_some()\n          || diff.branch.is_some()\n          || diff.commit.is_some();\n        if changed {\n          cache.insert(\n            target,\n            Some((String::from(\"stack config has changed\"), after)),\n          );\n          return Ok(());\n        }\n      }\n      // All other cases will require Deploy to enter Running state.\n      _ => {\n        cache.insert(\n          target,\n          Some((\n            format!(\"stack has {} state\", colored(state, Color::Red)),\n            after,\n          )),\n        );\n        return Ok(());\n      }\n    };\n\n    // Check 'after' to see if they deploy.\n    insert_target_using_after_list(\n      target,\n      after,\n      SyncDeployParams {\n        deployments,\n        deployment_map,\n        stacks,\n        stack_map,\n      },\n      cache,\n      build_version_cache,\n    )\n    .await\n  })\n}\n\nasync fn insert_target_using_after_list<'a>(\n  target: ResourceTarget,\n  after: Vec<ResourceTarget>,\n  SyncDeployParams {\n    deployments,\n    deployment_map,\n    stacks,\n    stack_map,\n  }: SyncDeployParams<'a>,\n  cache: &'a mut ToDeployCacheInner,\n  build_version_cache: &'a mut BuildVersionCache,\n) -> anyhow::Result<()> {\n  for parent in &after {\n    match cache.get(parent) {\n      Some(Some(_)) => {\n        // a parent will deploy\n        let (variant, name) = parent.extract_variant_id();\n        cache.insert(\n          target.to_owned(),\n          Some((\n            format!(\n              \"{variant} parent dependency '{}' is deploying\",\n              bold(name)\n            ),\n            after,\n          )),\n        );\n        return Ok(());\n      }\n      // The parent will not deploy, do nothing here.\n      Some(None) => {}\n      None => {\n        match parent {\n          ResourceTarget::Deployment(name) => {\n            let Some(parent_deployment) =\n              deployments.iter().find(|d| &d.name == name)\n            else {\n              // The parent is not in the sync, so won't be deploying\n              // Note that cross-sync deploy dependencies are not currently supported.\n              continue;\n            };\n            // Recurse to add the parent to cache, then check again.\n            build_cache_for_deployment(\n              parent_deployment,\n              SyncDeployParams {\n                deployments,\n                deployment_map,\n                stacks,\n                stack_map,\n              },\n              cache,\n              build_version_cache,\n            )\n            .await?;\n            match cache.get(parent) {\n              Some(Some(_)) => {\n                // Same as the 'Some' case above\n                let (variant, name) = parent.extract_variant_id();\n                cache.insert(\n                  target.to_owned(),\n                  Some((\n                    format!(\n                      \"{variant} parent dependency '{}' is deploying\",\n                      bold(name)\n                    ),\n                    after,\n                  )),\n                );\n                return Ok(());\n              }\n              // The parent will not deploy, do nothing here.\n              Some(None) => {}\n              None => {\n                return Err(anyhow!(\n                  \"Did not find parent in cache after build recursion. This should not happen.\"\n                ));\n              }\n            }\n          }\n          ResourceTarget::Stack(name) => {\n            let Some(parent_stack) =\n              stacks.iter().find(|d| &d.name == name)\n            else {\n              // The parent is not in the sync, so won't be deploying\n              // Note that cross-sync deploy dependencies are not currently supported.\n              continue;\n            };\n            // Recurse to add the parent to cache, then check again.\n            build_cache_for_stack(\n              parent_stack,\n              SyncDeployParams {\n                deployments,\n                deployment_map,\n                stacks,\n                stack_map,\n              },\n              cache,\n              build_version_cache,\n            )\n            .await?;\n            match cache.get(parent) {\n              Some(Some(_)) => {\n                // Same as the 'Some' case above\n                let (variant, name) = parent.extract_variant_id();\n                cache.insert(\n                  target.to_owned(),\n                  Some((\n                    format!(\n                      \"{variant} parent dependency '{}' is deploying\",\n                      bold(name)\n                    ),\n                    after,\n                  )),\n                );\n                return Ok(());\n              }\n              // The parent will not deploy, do nothing here.\n              Some(None) => {}\n              None => {\n                return Err(anyhow!(\n                  \"Did not find parent in cache after build recursion. This should not happen.\"\n                ));\n              }\n            }\n          }\n          _ => unreachable!(),\n        }\n      }\n    }\n  }\n\n  // If it has reached here, its not deploying\n  cache.insert(target, None);\n  Ok(())\n}\n\nfn get_after_as_resource_targets(\n  resource_name: &str,\n  after: &[String],\n  // Names to deployments\n  deployment_map: &HashMap<String, Deployment>,\n  deployments: &[ResourceToml<PartialDeploymentConfig>],\n  // Names to stacks\n  stack_map: &HashMap<String, Stack>,\n  stacks: &[ResourceToml<PartialStackConfig>],\n) -> anyhow::Result<Vec<ResourceTarget>> {\n  after\n    .iter()\n    .map(|name| match deployment_map.get(name) {\n      Some(_) => Ok(ResourceTarget::Deployment(name.clone())),\n      None => {\n        if deployments\n          .iter()\n          .any(|deployment| deployment.name.as_str() == resource_name)\n        {\n          Ok(ResourceTarget::Deployment(name.clone()))\n        } else {\n          match stack_map.get(name) {\n            Some(_) => Ok(ResourceTarget::Stack(name.clone())),\n            None => {\n              if stacks\n                .iter()\n                .any(|stack| stack.name.as_str() == resource_name)\n              {\n                Ok(ResourceTarget::Stack(name.clone()))\n              } else {\n                Err(anyhow!(\"failed to match deploy dependency in 'after' list | resource: {resource_name} | dependency: {name}\"))\n              }\n            }\n          }\n        }\n      }\n    })\n    .collect()\n}\n"
  },
  {
    "path": "bin/core/src/sync/execute.rs",
    "content": "use std::collections::HashMap;\n\nuse anyhow::Context;\nuse database::mungos::find::find_collect;\nuse formatting::{Color, bold, colored, muted};\nuse komodo_client::entities::{\n  ResourceTargetVariant, tag::Tag, toml::ResourceToml, update::Log,\n  user::sync_user,\n};\nuse partial_derive2::MaybeNone;\n\nuse crate::{api::write::WriteArgs, resource::ResourceMetaUpdate};\n\nuse super::{ResourceSyncTrait, SyncDeltas, ToUpdateItem};\n\n/// Gets all the resources to update. For use in sync execution.\npub async fn get_updates_for_execution<\n  Resource: ResourceSyncTrait,\n>(\n  resources: Vec<ResourceToml<Resource::PartialConfig>>,\n  delete: bool,\n  match_resource_type: Option<ResourceTargetVariant>,\n  match_resources: Option<&[String]>,\n  id_to_tags: &HashMap<String, Tag>,\n  match_tags: &[String],\n) -> anyhow::Result<SyncDeltas<Resource::PartialConfig>> {\n  let map = find_collect(Resource::coll(), None, None)\n    .await\n    .context(\"failed to get resources from db\")?\n    .into_iter()\n    .filter(|r| {\n      Resource::include_resource(\n        &r.name,\n        &r.config,\n        match_resource_type,\n        match_resources,\n        &r.tags,\n        id_to_tags,\n        match_tags,\n      )\n    })\n    .map(|r| (r.name.clone(), r))\n    .collect::<HashMap<_, _>>();\n  let resources = resources\n    .into_iter()\n    .filter(|r| {\n      Resource::include_resource_partial(\n        &r.name,\n        &r.config,\n        match_resource_type,\n        match_resources,\n        &r.tags,\n        id_to_tags,\n        match_tags,\n      )\n    })\n    .collect::<Vec<_>>();\n\n  let mut deltas = SyncDeltas::<Resource::PartialConfig>::default();\n\n  if delete {\n    for resource in map.values() {\n      if !resources.iter().any(|r| r.name == resource.name) {\n        deltas.to_delete.push(resource.name.clone());\n      }\n    }\n  }\n\n  for mut resource in resources {\n    match map.get(&resource.name) {\n      Some(original) => {\n        // First merge toml resource config (partial) onto default resource config.\n        // Makes sure things that aren't defined in toml (come through as None) actually get removed.\n        let config: Resource::Config = resource.config.into();\n        resource.config = config.into();\n\n        Resource::validate_partial_config(&mut resource.config);\n\n        let mut diff = Resource::get_diff(\n          original.config.clone(),\n          resource.config,\n        )?;\n\n        Resource::validate_diff(&mut diff);\n\n        let original_tags = original\n          .tags\n          .iter()\n          .filter_map(|id| id_to_tags.get(id).map(|t| t.name.clone()))\n          .collect::<Vec<_>>();\n\n        // Only proceed if there are any fields to update,\n        // or a change to tags / description\n        if diff.is_none()\n          && resource.description == original.description\n          && resource.template == original.template\n          && resource.tags == original_tags\n        {\n          continue;\n        }\n\n        // Minimizes updates through diffing.\n        resource.config = diff.into();\n\n        let update = ToUpdateItem {\n          id: original.id.clone(),\n          update_description: resource.description\n            != original.description,\n          update_template: resource.template != original.template,\n          update_tags: resource.tags != original_tags,\n          resource,\n        };\n\n        deltas.to_update.push(update);\n      }\n      None => deltas.to_create.push(resource),\n    }\n  }\n\n  Ok(deltas)\n}\n\npub trait ExecuteResourceSync: ResourceSyncTrait {\n  async fn execute_sync_updates(\n    SyncDeltas {\n      to_create,\n      to_update,\n      to_delete,\n    }: SyncDeltas<Self::PartialConfig>,\n  ) -> Option<Log> {\n    if to_create.is_empty()\n      && to_update.is_empty()\n      && to_delete.is_empty()\n    {\n      return None;\n    }\n\n    let mut has_error = false;\n    let mut log =\n      format!(\"running updates on {}s\", Self::resource_type());\n\n    for resource in to_create {\n      let name = resource.name.clone();\n      let id = match crate::resource::create::<Self>(\n        &resource.name,\n        resource.config,\n        sync_user(),\n      )\n      .await\n      .map_err(|e| e.error)\n      {\n        Ok(resource) => resource.id,\n        Err(e) => {\n          has_error = true;\n          log.push_str(&format!(\n            \"\\n{}: failed to create {} '{}' | {e:#}\",\n            colored(\"ERROR\", Color::Red),\n            Self::resource_type(),\n            bold(&name)\n          ));\n          continue;\n        }\n      };\n      run_update_meta::<Self>(\n        id.clone(),\n        &name,\n        ResourceMetaUpdate {\n          description: Some(resource.description),\n          template: Some(resource.template),\n          tags: Some(resource.tags),\n        },\n        &mut log,\n        &mut has_error,\n      )\n      .await;\n      log.push_str(&format!(\n        \"\\n{}: {} {} '{}'\",\n        muted(\"INFO\"),\n        colored(\"created\", Color::Green),\n        Self::resource_type(),\n        bold(&name)\n      ));\n    }\n\n    for ToUpdateItem {\n      id,\n      resource,\n      update_description,\n      update_template,\n      update_tags,\n    } in to_update\n    {\n      let name = resource.name.clone();\n\n      let meta = ResourceMetaUpdate {\n        description: update_description\n          .then(|| resource.description.clone()),\n        template: update_template.then_some(resource.template),\n        tags: update_tags.then(|| resource.tags.clone()),\n      };\n\n      if !meta.is_none() {\n        run_update_meta::<Self>(\n          id.clone(),\n          &name,\n          meta,\n          &mut log,\n          &mut has_error,\n        )\n        .await;\n      }\n\n      if !resource.config.is_none() {\n        if let Err(e) = crate::resource::update::<Self>(\n          &id,\n          resource.config,\n          sync_user(),\n        )\n        .await\n        {\n          has_error = true;\n          log.push_str(&format!(\n            \"\\n{}: failed to update config on {} '{}' | {e:#}\",\n            colored(\"ERROR\", Color::Red),\n            Self::resource_type(),\n            bold(&name),\n          ))\n        } else {\n          log.push_str(&format!(\n            \"\\n{}: {} {} '{}' configuration\",\n            muted(\"INFO\"),\n            colored(\"updated\", Color::Blue),\n            Self::resource_type(),\n            bold(&name)\n          ));\n        }\n      }\n    }\n\n    for resource in to_delete {\n      if let Err(e) = crate::resource::delete::<Self>(\n        &resource,\n        &WriteArgs {\n          user: sync_user().to_owned(),\n        },\n      )\n      .await\n      {\n        has_error = true;\n        log.push_str(&format!(\n          \"\\n{}: failed to delete {} '{}' | {e:#}\",\n          colored(\"ERROR\", Color::Red),\n          Self::resource_type(),\n          bold(&resource),\n        ))\n      } else {\n        log.push_str(&format!(\n          \"\\n{}: {} {} '{}'\",\n          muted(\"INFO\"),\n          colored(\"deleted\", Color::Red),\n          Self::resource_type(),\n          bold(&resource)\n        ));\n      }\n    }\n\n    let stage = format!(\"Update {}s\", Self::resource_type());\n    Some(if has_error {\n      Log::error(&stage, log)\n    } else {\n      Log::simple(&stage, log)\n    })\n  }\n}\n\npub async fn run_update_meta<Resource: ResourceSyncTrait>(\n  id: String,\n  name: &str,\n  meta: ResourceMetaUpdate,\n  log: &mut String,\n  has_error: &mut bool,\n) {\n  if let Err(e) = crate::resource::update_meta::<Resource>(\n    &id,\n    meta,\n    &WriteArgs {\n      user: sync_user().to_owned(),\n    },\n  )\n  .await\n  {\n    *has_error = true;\n    log.push_str(&format!(\n      \"\\n{}: failed to update tags on {} '{}' | {:#}\",\n      colored(\"ERROR\", Color::Red),\n      Resource::resource_type(),\n      bold(name),\n      e\n    ))\n  } else {\n    log.push_str(&format!(\n      \"\\n{}: {} {} '{}' meta\",\n      muted(\"INFO\"),\n      colored(\"updated\", Color::Blue),\n      Resource::resource_type(),\n      bold(name)\n    ));\n  }\n}\n"
  },
  {
    "path": "bin/core/src/sync/file.rs",
    "content": "use std::{\n  fs,\n  path::{Path, PathBuf},\n};\n\nuse anyhow::{Context, anyhow};\nuse formatting::{Color, bold, colored, format_serror, muted};\nuse komodo_client::entities::{\n  sync::SyncFileContents,\n  toml::{ResourceToml, ResourcesToml},\n  update::Log,\n};\n\npub fn read_resources(\n  root_path: &Path,\n  resource_path: &[String],\n  match_tags: &[String],\n  logs: &mut Vec<Log>,\n  files: &mut Vec<SyncFileContents>,\n  file_errors: &mut Vec<SyncFileContents>,\n) -> anyhow::Result<ResourcesToml> {\n  let mut resources = ResourcesToml::default();\n\n  for resource_path in resource_path {\n    let resource_path = resource_path\n      .parse::<PathBuf>()\n      .context(\"Invalid resource path\")?;\n    let full_path = root_path\n      .join(&resource_path)\n      .components()\n      .collect::<PathBuf>();\n\n    let mut log = format!(\n      \"{}: reading resources from {full_path:?}\",\n      muted(\"INFO\")\n    );\n\n    if full_path.is_file() {\n      if let Err(e) = read_resource_file(\n        root_path,\n        None,\n        &resource_path,\n        match_tags,\n        &mut resources,\n        &mut log,\n        files,\n      )\n      .with_context(|| {\n        format!(\"failed to read resources from {full_path:?}\")\n      }) {\n        file_errors.push(SyncFileContents {\n          resource_path: String::new(),\n          path: resource_path.display().to_string(),\n          contents: format_serror(&e.into()),\n        });\n        logs.push(Log::error(\"Read remote resources\", log));\n      } else {\n        logs.push(Log::simple(\"Read remote resources\", log));\n      };\n    } else if full_path.is_dir() {\n      if let Err(e) = read_resources_directory(\n        root_path,\n        &resource_path,\n        &PathBuf::new(),\n        match_tags,\n        &mut resources,\n        &mut log,\n        files,\n        file_errors,\n      )\n      .with_context(|| {\n        format!(\"Failed to read resources from {full_path:?}\")\n      }) {\n        file_errors.push(SyncFileContents {\n          resource_path: String::new(),\n          path: resource_path.display().to_string(),\n          contents: format_serror(&e.into()),\n        });\n        logs.push(Log::error(\"Read remote resources\", log));\n      } else {\n        logs.push(Log::simple(\"Read remote resources\", log));\n      };\n    } else if !full_path.exists() {\n      file_errors.push(SyncFileContents {\n        resource_path: String::new(),\n        path: resource_path.display().to_string(),\n        contents: format_serror(\n          &anyhow!(\"Initialize the file to proceed.\")\n            .context(format!(\"Path {full_path:?} does not exist.\"))\n            .into(),\n        ),\n      });\n      log.push_str(&format!(\n        \"{}: Resoure path {} does not exist.\",\n        colored(\"ERROR\", Color::Red),\n        bold(resource_path.display())\n      ));\n      logs.push(Log::error(\"Read remote resources\", log));\n    } else {\n      log.push_str(&format!(\n        \"{}: Resoure path {} exists, but is neither a file nor a directory.\",\n        colored(\"WARN\", Color::Red),\n        bold(resource_path.display())\n      ));\n      logs.push(Log::error(\"Read remote resources\", log));\n    }\n  }\n\n  Ok(resources)\n}\n\n/// Use when incoming resource path is a file.\nfn read_resource_file(\n  root_path: &Path,\n  // relative to root path.\n  resource_path: Option<&Path>,\n  // relative to resource path if provided, or root path.\n  file_path: &Path,\n  match_tags: &[String],\n  resources: &mut ResourcesToml,\n  log: &mut String,\n  files: &mut Vec<SyncFileContents>,\n) -> anyhow::Result<()> {\n  let full_path = if let Some(resource_path) = resource_path {\n    root_path.join(resource_path).join(file_path)\n  } else {\n    root_path.join(file_path)\n  };\n  if !full_path\n    .extension()\n    .map(|ext| ext == \"toml\")\n    .unwrap_or_default()\n  {\n    return Ok(());\n  }\n  let contents = std::fs::read_to_string(&full_path)\n    .context(\"failed to read file contents\")?;\n\n  files.push(SyncFileContents {\n    resource_path: resource_path\n      .map(|path| path.display().to_string())\n      .unwrap_or_default(),\n    path: file_path.display().to_string(),\n    contents: contents.clone(),\n  });\n  let more = super::deserialize_resources_toml(&contents)\n    .context(\"failed to parse resource file contents\")?;\n  log.push('\\n');\n  let path_for_view =\n    if let Some(resource_path) = resource_path.as_ref() {\n      resource_path.join(file_path)\n    } else {\n      file_path.to_path_buf()\n    };\n  log.push_str(&format!(\n    \"{}: {} from {}\",\n    muted(\"INFO\"),\n    colored(\"adding resources\", Color::Green),\n    colored(path_for_view.display(), Color::Blue)\n  ));\n\n  extend_resources(resources, more, match_tags);\n\n  Ok(())\n}\n\n/// Reads down into directories.\n#[allow(clippy::too_many_arguments)]\nfn read_resources_directory(\n  root_path: &Path,\n  // relative to root path.\n  resource_path: &Path,\n  // relative to resource path. start as empty path\n  curr_path: &Path,\n  match_tags: &[String],\n  resources: &mut ResourcesToml,\n  log: &mut String,\n  files: &mut Vec<SyncFileContents>,\n  file_errors: &mut Vec<SyncFileContents>,\n) -> anyhow::Result<()> {\n  let full_resource_path = root_path.join(resource_path);\n  let full_path = full_resource_path.join(curr_path);\n  let directory = fs::read_dir(&full_path).with_context(|| {\n    format!(\"Failed to read directory contents at {full_path:?}\")\n  })?;\n  for entry in directory.into_iter().flatten() {\n    let path = entry.path();\n    let curr_path =\n      path.strip_prefix(&full_resource_path).unwrap_or(&path);\n    if path.is_file() {\n      if let Err(e) = read_resource_file(\n        root_path,\n        Some(resource_path),\n        curr_path,\n        match_tags,\n        resources,\n        log,\n        files,\n      )\n      .with_context(|| {\n        format!(\"failed to read resources from {full_path:?}\")\n      }) {\n        file_errors.push(SyncFileContents {\n          resource_path: String::new(),\n          path: resource_path.display().to_string(),\n          contents: format_serror(&e.into()),\n        });\n      };\n    } else if path.is_dir()\n      && let Err(e) = read_resources_directory(\n        root_path,\n        resource_path,\n        curr_path,\n        match_tags,\n        resources,\n        log,\n        files,\n        file_errors,\n      )\n      .with_context(|| {\n        format!(\"failed to read resources from {path:?}\")\n      })\n    {\n      file_errors.push(SyncFileContents {\n        resource_path: resource_path.display().to_string(),\n        path: curr_path.display().to_string(),\n        contents: format_serror(&e.into()),\n      });\n      log.push('\\n');\n      log.push_str(&format!(\n        \"{}: {} from {}\",\n        colored(\"ERROR\", Color::Red),\n        colored(\"adding resources\", Color::Green),\n        colored(path.display(), Color::Blue)\n      ));\n    }\n  }\n  Ok(())\n}\n\npub fn extend_resources(\n  resources: &mut ResourcesToml,\n  more: ResourcesToml,\n  match_tags: &[String],\n) {\n  resources\n    .servers\n    .extend(filter_by_tag(more.servers, match_tags));\n  resources\n    .stacks\n    .extend(filter_by_tag(more.stacks, match_tags));\n  resources\n    .deployments\n    .extend(filter_by_tag(more.deployments, match_tags));\n  resources\n    .builds\n    .extend(filter_by_tag(more.builds, match_tags));\n  resources\n    .repos\n    .extend(filter_by_tag(more.repos, match_tags));\n  resources\n    .procedures\n    .extend(filter_by_tag(more.procedures, match_tags));\n  resources\n    .actions\n    .extend(filter_by_tag(more.actions, match_tags));\n  resources\n    .alerters\n    .extend(filter_by_tag(more.alerters, match_tags));\n  resources\n    .builders\n    .extend(filter_by_tag(more.builders, match_tags));\n  resources\n    .resource_syncs\n    .extend(filter_by_tag(more.resource_syncs, match_tags));\n  resources.user_groups.extend(more.user_groups);\n  resources.variables.extend(more.variables);\n}\n\nfn filter_by_tag<T: Default>(\n  resources: Vec<ResourceToml<T>>,\n  match_tags: &[String],\n) -> Vec<ResourceToml<T>> {\n  resources\n    .into_iter()\n    .filter(|resource| {\n      match_tags.iter().all(|tag| resource.tags.contains(tag))\n    })\n    .collect()\n}\n"
  },
  {
    "path": "bin/core/src/sync/mod.rs",
    "content": "use std::{collections::HashMap, str::FromStr};\n\nuse anyhow::anyhow;\nuse database::mungos::mongodb::bson::oid::ObjectId;\nuse komodo_client::entities::{\n  ResourceTargetVariant,\n  tag::Tag,\n  toml::{ResourceToml, ResourcesToml},\n};\nuse toml::ToToml;\n\nuse crate::resource::KomodoResource;\n\npub mod deploy;\npub mod execute;\npub mod file;\npub mod remote;\npub mod resources;\npub mod toml;\npub mod user_groups;\npub mod variables;\npub mod view;\n\n#[derive(Default)]\npub struct SyncDeltas<T: Default> {\n  pub to_create: Vec<ResourceToml<T>>,\n  pub to_update: Vec<ToUpdateItem<T>>,\n  pub to_delete: Vec<String>,\n}\n\nimpl<T: Default> SyncDeltas<T> {\n  pub fn no_changes(&self) -> bool {\n    self.to_create.is_empty()\n      && self.to_update.is_empty()\n      && self.to_delete.is_empty()\n  }\n}\n\npub struct ToUpdateItem<T: Default> {\n  pub id: String,\n  pub resource: ResourceToml<T>,\n  pub update_description: bool,\n  pub update_template: bool,\n  pub update_tags: bool,\n}\n\npub trait ResourceSyncTrait: ToToml + Sized {\n  /// To exclude resource syncs with \"file_contents\" (they aren't compatible)\n  fn include_resource(\n    name: &String,\n    _config: &Self::Config,\n    match_resource_type: Option<ResourceTargetVariant>,\n    match_resources: Option<&[String]>,\n    resource_tags: &[String],\n    id_to_tags: &HashMap<String, Tag>,\n    match_tags: &[String],\n  ) -> bool {\n    include_resource_by_resource_type_and_name::<Self>(\n      match_resource_type,\n      match_resources,\n      name,\n    ) && include_resource_by_tags(\n      resource_tags,\n      id_to_tags,\n      match_tags,\n    )\n  }\n\n  /// To exclude resource syncs with \"file_contents\" (they aren't compatible)\n  fn include_resource_partial(\n    name: &String,\n    _config: &Self::PartialConfig,\n    match_resource_type: Option<ResourceTargetVariant>,\n    match_resources: Option<&[String]>,\n    resource_tags: &[String],\n    id_to_tags: &HashMap<String, Tag>,\n    match_tags: &[String],\n  ) -> bool {\n    include_resource_by_resource_type_and_name::<Self>(\n      match_resource_type,\n      match_resources,\n      name,\n    ) && include_resource_by_tags(\n      resource_tags,\n      id_to_tags,\n      match_tags,\n    )\n  }\n\n  /// Apply any changes to incoming toml partial config\n  /// before it is diffed against existing config\n  fn validate_partial_config(_config: &mut Self::PartialConfig) {}\n\n  /// Diffs the declared toml (partial) against the full existing config.\n  /// Removes all fields from toml (partial) that haven't changed.\n  fn get_diff(\n    original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff>;\n\n  /// Apply any changes to computed config diff\n  /// before logging\n  fn validate_diff(_diff: &mut Self::ConfigDiff) {}\n}\n\npub fn include_resource_by_tags(\n  resource_tags: &[String],\n  id_to_tags: &HashMap<String, Tag>,\n  match_tags: &[String],\n) -> bool {\n  let tag_names = resource_tags\n    .iter()\n    .filter_map(|resource_tag| {\n      match ObjectId::from_str(resource_tag) {\n        Ok(_) => id_to_tags.get(resource_tag).map(|tag| &tag.name),\n        Err(_) => Some(resource_tag),\n      }\n    })\n    .collect::<Vec<_>>();\n  match_tags.iter().all(|tag| tag_names.contains(&tag))\n}\n\npub fn include_resource_by_resource_type_and_name<\n  T: KomodoResource,\n>(\n  resource_type: Option<ResourceTargetVariant>,\n  resources: Option<&[String]>,\n  name: &String,\n) -> bool {\n  match (resource_type, resources) {\n    (Some(resource_type), Some(resources)) => {\n      if T::resource_type() != resource_type {\n        return false;\n      }\n      resources.contains(name)\n    }\n    (Some(resource_type), None) => {\n      if T::resource_type() != resource_type {\n        return false;\n      }\n      true\n    }\n    (None, Some(resources)) => resources.contains(name),\n    (None, None) => true,\n  }\n}\n\nfn deserialize_resources_toml(\n  toml_str: &str,\n) -> anyhow::Result<ResourcesToml> {\n  ::toml::from_str::<ResourcesToml>(&escape_between_triple_string(\n    toml_str,\n  ))\n  // the error without this comes through with multiple lines (\\n) and looks bad\n  .map_err(|e| anyhow!(\"{e:#}\"))\n}\n\nfn escape_between_triple_string(toml_str: &str) -> String {\n  toml_str\n    .split(r#\"\"\"\"\"#)\n    .enumerate()\n    .map(|(i, section)| {\n      // The odd entries are between triple string,\n      // and the \\ need to be escaped.\n      if i % 2 == 0 {\n        section.to_string()\n      } else {\n        section.replace(r#\"\\\"#, r#\"\\\\\"#)\n      }\n    })\n    .collect::<Vec<_>>()\n    .join(r#\"\"\"\"\"#)\n}\n"
  },
  {
    "path": "bin/core/src/sync/remote.rs",
    "content": "use anyhow::Context;\nuse komodo_client::entities::{\n  RepoExecutionArgs, RepoExecutionResponse,\n  repo::Repo,\n  sync::{ResourceSync, SyncFileContents},\n  to_path_compatible_name,\n  toml::ResourcesToml,\n  update::Log,\n};\n\nuse crate::{config::core_config, helpers::git_token};\n\nuse super::file::extend_resources;\n\npub struct RemoteResources {\n  pub resources: anyhow::Result<ResourcesToml>,\n  pub files: Vec<SyncFileContents>,\n  pub file_errors: Vec<SyncFileContents>,\n  pub logs: Vec<Log>,\n  pub hash: Option<String>,\n  pub message: Option<String>,\n}\n\n/// Use `match_tags` to filter resources by tag.\npub async fn get_remote_resources(\n  sync: &ResourceSync,\n  repo: Option<&Repo>,\n) -> anyhow::Result<RemoteResources> {\n  if sync.config.files_on_host {\n    get_files_on_host(sync).await\n  } else if let Some(repo) = repo {\n    get_repo(sync, repo.into()).await\n  } else if !sync.config.repo.is_empty() {\n    get_repo(sync, sync.into()).await\n  } else {\n    get_ui_defined(sync).await\n  }\n}\n\nasync fn get_files_on_host(\n  sync: &ResourceSync,\n) -> anyhow::Result<RemoteResources> {\n  let root_path = core_config()\n    .sync_directory\n    .join(to_path_compatible_name(&sync.name));\n  let (mut logs, mut files, mut file_errors) =\n    (Vec::new(), Vec::new(), Vec::new());\n  let resources = super::file::read_resources(\n    &root_path,\n    &sync.config.resource_path,\n    &sync.config.match_tags,\n    &mut logs,\n    &mut files,\n    &mut file_errors,\n  );\n  Ok(RemoteResources {\n    resources,\n    files,\n    file_errors,\n    logs,\n    hash: None,\n    message: None,\n  })\n}\n\nasync fn get_repo(\n  sync: &ResourceSync,\n  mut clone_args: RepoExecutionArgs,\n) -> anyhow::Result<RemoteResources> {\n  let access_token = if let Some(account) = &clone_args.account {\n    git_token(&clone_args.provider, account, |https| clone_args.https = https)\n      .await\n      .with_context(\n        || format!(\"Failed to get git token in call to db. Stopping run. | {} | {account}\", clone_args.provider),\n      )?\n  } else {\n    None\n  };\n\n  let repo_path =\n    clone_args.unique_path(&core_config().repo_directory)?;\n  clone_args.destination = Some(repo_path.display().to_string());\n\n  let (\n    RepoExecutionResponse {\n      mut logs,\n      commit_hash,\n      commit_message,\n      ..\n    },\n    _,\n  ) = git::pull_or_clone(\n    clone_args,\n    &core_config().repo_directory,\n    access_token,\n  )\n  .await\n  .with_context(|| {\n    format!(\"Failed to update resource repo at {repo_path:?}\")\n  })?;\n\n  // let hash = hash.context(\"failed to get commit hash\")?;\n  // let message =\n  //   message.context(\"failed to get commit hash message\")?;\n\n  let (mut files, mut file_errors) = (Vec::new(), Vec::new());\n  let resources = super::file::read_resources(\n    &repo_path,\n    &sync.config.resource_path,\n    &sync.config.match_tags,\n    &mut logs,\n    &mut files,\n    &mut file_errors,\n  );\n\n  Ok(RemoteResources {\n    resources,\n    files,\n    file_errors,\n    logs,\n    hash: commit_hash,\n    message: commit_message,\n  })\n}\n\nasync fn get_ui_defined(\n  sync: &ResourceSync,\n) -> anyhow::Result<RemoteResources> {\n  let mut resources = ResourcesToml::default();\n  let resources =\n    super::deserialize_resources_toml(&sync.config.file_contents)\n      .context(\"failed to parse resource file contents\")\n      .map(|more| {\n        extend_resources(\n          &mut resources,\n          more,\n          &sync.config.match_tags,\n        );\n        resources\n      });\n\n  Ok(RemoteResources {\n    resources,\n    files: vec![SyncFileContents {\n      resource_path: String::new(),\n      path: \"database file\".to_string(),\n      contents: sync.config.file_contents.clone(),\n    }],\n    file_errors: vec![],\n    logs: vec![Log::simple(\n      \"Read from database\",\n      \"Resources added from database file\".to_string(),\n    )],\n    hash: None,\n    message: None,\n  })\n}\n"
  },
  {
    "path": "bin/core/src/sync/resources.rs",
    "content": "use std::collections::HashMap;\n\nuse formatting::{Color, bold, colored, muted};\nuse komodo_client::{\n  api::execute::Execution,\n  entities::{\n    ResourceTargetVariant,\n    action::Action,\n    alerter::Alerter,\n    build::Build,\n    builder::{Builder, BuilderConfig},\n    deployment::{Deployment, DeploymentImage},\n    procedure::Procedure,\n    repo::Repo,\n    server::Server,\n    stack::Stack,\n    sync::ResourceSync,\n    tag::Tag,\n    update::Log,\n    user::sync_user,\n  },\n};\nuse partial_derive2::{MaybeNone, PartialDiff};\n\nuse crate::{\n  api::write::WriteArgs,\n  resource::{KomodoResource, ResourceMetaUpdate},\n  state::all_resources_cache,\n  sync::{ToUpdateItem, execute::run_update_meta},\n};\n\nuse super::{\n  ResourceSyncTrait, SyncDeltas, execute::ExecuteResourceSync,\n  include_resource_by_resource_type_and_name,\n  include_resource_by_tags,\n};\n\nimpl ResourceSyncTrait for Server {\n  fn get_diff(\n    original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff> {\n    Ok(original.partial_diff(update))\n  }\n}\n\nimpl ExecuteResourceSync for Server {}\n\nimpl ResourceSyncTrait for Deployment {\n  fn get_diff(\n    mut original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff> {\n    let resources = all_resources_cache().load();\n    // need to replace the server id with name\n    original.server_id = resources\n      .servers\n      .get(&original.server_id)\n      .map(|s| s.name.clone())\n      .unwrap_or_default();\n\n    // need to replace the build id with name\n    if let DeploymentImage::Build { build_id, version } =\n      &original.image\n    {\n      original.image = DeploymentImage::Build {\n        build_id: resources\n          .builds\n          .get(build_id)\n          .map(|b| b.name.clone())\n          .unwrap_or_default(),\n        version: *version,\n      };\n    }\n\n    Ok(original.partial_diff(update))\n  }\n}\n\nimpl ExecuteResourceSync for Deployment {}\n\nimpl ResourceSyncTrait for Stack {\n  fn get_diff(\n    mut original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff> {\n    let resources = all_resources_cache().load();\n    // Need to replace server id with name\n    original.server_id = resources\n      .servers\n      .get(&original.server_id)\n      .map(|s| s.name.clone())\n      .unwrap_or_default();\n    // Replace linked repo with name\n    original.linked_repo = resources\n      .repos\n      .get(&original.linked_repo)\n      .map(|r| r.name.clone())\n      .unwrap_or_default();\n\n    Ok(original.partial_diff(update))\n  }\n}\n\nimpl ExecuteResourceSync for Stack {}\n\nimpl ResourceSyncTrait for Build {\n  fn get_diff(\n    mut original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff> {\n    let resources = all_resources_cache().load();\n    original.builder_id = resources\n      .builders\n      .get(&original.builder_id)\n      .map(|b| b.name.clone())\n      .unwrap_or_default();\n    original.linked_repo = resources\n      .repos\n      .get(&original.linked_repo)\n      .map(|r| r.name.clone())\n      .unwrap_or_default();\n\n    Ok(original.partial_diff(update))\n  }\n\n  fn validate_diff(diff: &mut Self::ConfigDiff) {\n    if let Some((_, to)) = &diff.version {\n      // When setting a build back to \"latest\" version,\n      // Don't actually set version to None.\n      // You can do this on the db, or set it to 0.0.1\n      if to.is_none() {\n        diff.version = None;\n      }\n    }\n  }\n}\n\nimpl ExecuteResourceSync for Build {}\n\nimpl ResourceSyncTrait for Repo {\n  fn get_diff(\n    mut original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff> {\n    let resources = all_resources_cache().load();\n    // Need to replace server id with name\n    original.server_id = resources\n      .servers\n      .get(&original.server_id)\n      .map(|s| s.name.clone())\n      .unwrap_or_default();\n\n    // Need to replace builder id with name\n    original.builder_id = resources\n      .builders\n      .get(&original.builder_id)\n      .map(|s| s.name.clone())\n      .unwrap_or_default();\n\n    Ok(original.partial_diff(update))\n  }\n}\n\nimpl ExecuteResourceSync for Repo {}\n\nimpl ResourceSyncTrait for Alerter {\n  fn get_diff(\n    original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff> {\n    Ok(original.partial_diff(update))\n  }\n}\n\nimpl ExecuteResourceSync for Alerter {}\n\nimpl ResourceSyncTrait for Builder {\n  fn get_diff(\n    mut original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff> {\n    // need to replace server builder id with name\n    if let BuilderConfig::Server(config) = &mut original {\n      let resources = all_resources_cache().load();\n      config.server_id = resources\n        .servers\n        .get(&config.server_id)\n        .map(|s| s.name.clone())\n        .unwrap_or_default();\n    }\n\n    Ok(original.partial_diff(update))\n  }\n}\n\nimpl ExecuteResourceSync for Builder {}\n\nimpl ResourceSyncTrait for Action {\n  fn get_diff(\n    original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff> {\n    Ok(original.partial_diff(update))\n  }\n}\n\nimpl ExecuteResourceSync for Action {}\n\nimpl ResourceSyncTrait for ResourceSync {\n  fn include_resource(\n    name: &String,\n    config: &Self::Config,\n    match_resource_type: Option<ResourceTargetVariant>,\n    match_resources: Option<&[String]>,\n    resource_tags: &[String],\n    id_to_tags: &HashMap<String, Tag>,\n    match_tags: &[String],\n  ) -> bool {\n    if !include_resource_by_resource_type_and_name::<ResourceSync>(\n      match_resource_type,\n      match_resources,\n      name,\n    ) || !include_resource_by_tags(\n      resource_tags,\n      id_to_tags,\n      match_tags,\n    ) {\n      return false;\n    }\n    // don't include fresh sync\n    let contents_empty = config.file_contents.is_empty();\n    if contents_empty\n      && !config.files_on_host\n      && config.repo.is_empty()\n      && config.linked_repo.is_empty()\n    {\n      return false;\n    }\n    // The file contents MUST be empty\n    contents_empty &&\n    // The sync must be files on host mode OR git repo mode\n    (config.files_on_host || !config.repo.is_empty() || !config.linked_repo.is_empty())\n  }\n\n  fn include_resource_partial(\n    name: &String,\n    config: &Self::PartialConfig,\n    match_resource_type: Option<ResourceTargetVariant>,\n    match_resources: Option<&[String]>,\n    resource_tags: &[String],\n    id_to_tags: &HashMap<String, Tag>,\n    match_tags: &[String],\n  ) -> bool {\n    if !include_resource_by_resource_type_and_name::<ResourceSync>(\n      match_resource_type,\n      match_resources,\n      name,\n    ) || !include_resource_by_tags(\n      resource_tags,\n      id_to_tags,\n      match_tags,\n    ) {\n      return false;\n    }\n    // don't include fresh sync\n    let contents_empty = config\n      .file_contents\n      .as_ref()\n      .map(String::is_empty)\n      .unwrap_or(true);\n    let files_on_host = config.files_on_host.unwrap_or_default();\n    if contents_empty\n      && !files_on_host\n      && config.repo.as_ref().map(String::is_empty).unwrap_or(true)\n      && config\n        .linked_repo\n        .as_ref()\n        .map(String::is_empty)\n        .unwrap_or(true)\n    {\n      return false;\n    }\n    // The file contents MUST be empty\n    contents_empty &&\n    // The sync must be files on host mode OR git repo mode\n    (files_on_host || !config.repo.as_deref().unwrap_or_default().is_empty() || !config.linked_repo.as_deref().unwrap_or_default().is_empty())\n  }\n\n  fn get_diff(\n    mut original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff> {\n    let resources = all_resources_cache().load();\n    original.linked_repo = resources\n      .repos\n      .get(&original.linked_repo)\n      .map(|r| r.name.clone())\n      .unwrap_or_default();\n\n    Ok(original.partial_diff(update))\n  }\n}\n\nimpl ExecuteResourceSync for ResourceSync {}\n\nimpl ResourceSyncTrait for Procedure {\n  fn get_diff(\n    mut original: Self::Config,\n    update: Self::PartialConfig,\n  ) -> anyhow::Result<Self::ConfigDiff> {\n    let resources = all_resources_cache().load();\n    for stage in &mut original.stages {\n      for execution in &mut stage.executions {\n        match &mut execution.execution {\n          Execution::None(_) => {}\n          Execution::RunProcedure(config) => {\n            config.procedure = resources\n              .procedures\n              .get(&config.procedure)\n              .map(|p| p.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchRunProcedure(_config) => {}\n          Execution::RunAction(config) => {\n            config.action = resources\n              .actions\n              .get(&config.action)\n              .map(|p| p.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchRunAction(_config) => {}\n          Execution::RunBuild(config) => {\n            config.build = resources\n              .builds\n              .get(&config.build)\n              .map(|b| b.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchRunBuild(_config) => {}\n          Execution::CancelBuild(config) => {\n            config.build = resources\n              .builds\n              .get(&config.build)\n              .map(|b| b.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::Deploy(config) => {\n            config.deployment = resources\n              .deployments\n              .get(&config.deployment)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchDeploy(_config) => {}\n          Execution::PullDeployment(config) => {\n            config.deployment = resources\n              .deployments\n              .get(&config.deployment)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::StartDeployment(config) => {\n            config.deployment = resources\n              .deployments\n              .get(&config.deployment)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::RestartDeployment(config) => {\n            config.deployment = resources\n              .deployments\n              .get(&config.deployment)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PauseDeployment(config) => {\n            config.deployment = resources\n              .deployments\n              .get(&config.deployment)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::UnpauseDeployment(config) => {\n            config.deployment = resources\n              .deployments\n              .get(&config.deployment)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::StopDeployment(config) => {\n            config.deployment = resources\n              .deployments\n              .get(&config.deployment)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::DestroyDeployment(config) => {\n            config.deployment = resources\n              .deployments\n              .get(&config.deployment)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchDestroyDeployment(_config) => {}\n          Execution::CloneRepo(config) => {\n            config.repo = resources\n              .repos\n              .get(&config.repo)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchCloneRepo(_config) => {}\n          Execution::PullRepo(config) => {\n            config.repo = resources\n              .repos\n              .get(&config.repo)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchPullRepo(_config) => {}\n          Execution::BuildRepo(config) => {\n            config.repo = resources\n              .repos\n              .get(&config.repo)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchBuildRepo(_config) => {}\n          Execution::CancelRepoBuild(config) => {\n            config.repo = resources\n              .repos\n              .get(&config.repo)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::StartContainer(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::RestartContainer(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PauseContainer(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::UnpauseContainer(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::StopContainer(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::DestroyContainer(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::StartAllContainers(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::RestartAllContainers(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PauseAllContainers(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::UnpauseAllContainers(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::StopAllContainers(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PruneContainers(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::DeleteNetwork(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PruneNetworks(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::DeleteImage(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PruneImages(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::DeleteVolume(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PruneVolumes(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PruneDockerBuilders(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PruneBuildx(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PruneSystem(config) => {\n            config.server = resources\n              .servers\n              .get(&config.server)\n              .map(|d| d.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::RunSync(config) => {\n            config.sync = resources\n              .syncs\n              .get(&config.sync)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::CommitSync(config) => {\n            config.sync = resources\n              .syncs\n              .get(&config.sync)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::DeployStack(config) => {\n            config.stack = resources\n              .stacks\n              .get(&config.stack)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchDeployStack(_config) => {}\n          Execution::DeployStackIfChanged(config) => {\n            config.stack = resources\n              .stacks\n              .get(&config.stack)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchDeployStackIfChanged(_config) => {}\n          Execution::PullStack(config) => {\n            config.stack = resources\n              .stacks\n              .get(&config.stack)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchPullStack(_config) => {}\n          Execution::StartStack(config) => {\n            config.stack = resources\n              .stacks\n              .get(&config.stack)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::RestartStack(config) => {\n            config.stack = resources\n              .stacks\n              .get(&config.stack)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::PauseStack(config) => {\n            config.stack = resources\n              .stacks\n              .get(&config.stack)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::UnpauseStack(config) => {\n            config.stack = resources\n              .stacks\n              .get(&config.stack)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::StopStack(config) => {\n            config.stack = resources\n              .stacks\n              .get(&config.stack)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::DestroyStack(config) => {\n            config.stack = resources\n              .stacks\n              .get(&config.stack)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::RunStackService(config) => {\n            config.stack = resources\n              .stacks\n              .get(&config.stack)\n              .map(|s| s.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::BatchDestroyStack(_config) => {}\n          Execution::TestAlerter(config) => {\n            config.alerter = resources\n              .alerters\n              .get(&config.alerter)\n              .map(|a| a.name.clone())\n              .unwrap_or_default();\n          }\n          Execution::SendAlert(config) => {\n            config.alerters = config\n              .alerters\n              .iter()\n              .map(|alerter| {\n                resources\n                  .alerters\n                  .get(alerter)\n                  .map(|a| a.name.clone())\n                  .unwrap_or_default()\n              })\n              .collect();\n          }\n          Execution::ClearRepoCache(_) => {}\n          Execution::BackupCoreDatabase(_) => {}\n          Execution::GlobalAutoUpdate(_) => {}\n          Execution::Sleep(_) => {}\n        }\n      }\n    }\n    Ok(original.partial_diff(update))\n  }\n}\n\nimpl ExecuteResourceSync for Procedure {\n  async fn execute_sync_updates(\n    SyncDeltas {\n      mut to_create,\n      mut to_update,\n      to_delete,\n    }: SyncDeltas<Self::PartialConfig>,\n  ) -> Option<Log> {\n    if to_create.is_empty()\n      && to_update.is_empty()\n      && to_delete.is_empty()\n    {\n      return None;\n    }\n\n    let mut has_error = false;\n    let mut log =\n      format!(\"running updates on {}s\", Self::resource_type());\n\n    for name in to_delete {\n      if let Err(e) = crate::resource::delete::<Procedure>(\n        &name,\n        &WriteArgs {\n          user: sync_user().to_owned(),\n        },\n      )\n      .await\n      {\n        has_error = true;\n        log.push_str(&format!(\n          \"\\n{}: failed to delete {} '{}' | {e:#}\",\n          colored(\"ERROR\", Color::Red),\n          Self::resource_type(),\n          bold(&name),\n        ))\n      } else {\n        log.push_str(&format!(\n          \"\\n{}: {} {} '{}'\",\n          muted(\"INFO\"),\n          colored(\"deleted\", Color::Red),\n          Self::resource_type(),\n          bold(&name)\n        ));\n      }\n    }\n\n    if to_update.is_empty() && to_create.is_empty() {\n      let stage = \"Update Procedures\";\n      return Some(if has_error {\n        Log::error(stage, log)\n      } else {\n        Log::simple(stage, log)\n      });\n    }\n\n    for i in 0..10 {\n      let mut to_pull = Vec::new();\n      for ToUpdateItem {\n        id,\n        resource,\n        update_description,\n        update_template,\n        update_tags,\n      } in &to_update\n      {\n        let name = resource.name.clone();\n\n        let meta = ResourceMetaUpdate {\n          description: update_description\n            .then(|| resource.description.clone()),\n          template: update_template.then(|| resource.template),\n          tags: update_tags.then(|| resource.tags.clone()),\n        };\n\n        if !meta.is_none() {\n          run_update_meta::<Procedure>(\n            id.clone(),\n            &name,\n            meta,\n            &mut log,\n            &mut has_error,\n          )\n          .await;\n        }\n        if !resource.config.is_none()\n          && let Err(e) = crate::resource::update::<Procedure>(\n            id,\n            resource.config.clone(),\n            sync_user(),\n          )\n          .await\n        {\n          if i == 9 {\n            has_error = true;\n            log.push_str(&format!(\n              \"\\n{}: failed to update {} '{}' | {e:#}\",\n              colored(\"ERROR\", Color::Red),\n              Self::resource_type(),\n              bold(&name)\n            ));\n          }\n          continue;\n        }\n\n        log.push_str(&format!(\n          \"\\n{}: {} '{}' updated\",\n          muted(\"INFO\"),\n          Self::resource_type(),\n          bold(&name)\n        ));\n        // have to clone out so to_update is mutable\n        to_pull.push(id.clone());\n      }\n      //\n      to_update.retain(|resource| !to_pull.contains(&resource.id));\n\n      let mut to_pull = Vec::new();\n      for resource in &to_create {\n        let name = resource.name.clone();\n        let id = match crate::resource::create::<Procedure>(\n          &name,\n          resource.config.clone(),\n          sync_user(),\n        )\n        .await\n        .map_err(|e| e.error)\n        {\n          Ok(resource) => resource.id,\n          Err(e) => {\n            if i == 9 {\n              has_error = true;\n              log.push_str(&format!(\n                \"\\n{}: failed to create {} '{}' | {e:#}\",\n                colored(\"ERROR\", Color::Red),\n                Self::resource_type(),\n                bold(&name)\n              ));\n            }\n            continue;\n          }\n        };\n        run_update_meta::<Procedure>(\n          id.clone(),\n          &name,\n          ResourceMetaUpdate {\n            description: Some(resource.description.clone()),\n            template: Some(resource.template),\n            tags: Some(resource.tags.clone()),\n          },\n          &mut log,\n          &mut has_error,\n        )\n        .await;\n        log.push_str(&format!(\n          \"\\n{}: {} {} '{}'\",\n          muted(\"INFO\"),\n          colored(\"created\", Color::Green),\n          Self::resource_type(),\n          bold(&name)\n        ));\n        to_pull.push(name);\n      }\n      to_create.retain(|resource| !to_pull.contains(&resource.name));\n\n      if to_update.is_empty() && to_create.is_empty() {\n        let stage = \"Update Procedures\";\n        return Some(if has_error {\n          Log::error(stage, log)\n        } else {\n          Log::simple(stage, log)\n        });\n      }\n    }\n    warn!(\"procedure sync loop exited after max iterations\");\n\n    Some(Log::error(\n      \"run procedure\",\n      String::from(\"procedure sync loop exited after max iterations\"),\n    ))\n  }\n}\n"
  },
  {
    "path": "bin/core/src/sync/toml.rs",
    "content": "use std::collections::HashMap;\n\nuse anyhow::Context;\nuse indexmap::IndexMap;\nuse komodo_client::{\n  api::execute::Execution,\n  entities::{\n    action::Action,\n    alerter::Alerter,\n    build::Build,\n    builder::{Builder, BuilderConfig, PartialBuilderConfig},\n    deployment::{Deployment, DeploymentImage},\n    procedure::Procedure,\n    repo::Repo,\n    resource::Resource,\n    server::Server,\n    stack::Stack,\n    sync::ResourceSync,\n    tag::Tag,\n    toml::ResourceToml,\n  },\n};\nuse partial_derive2::{MaybeNone, PartialDiff};\n\nuse crate::{resource::KomodoResource, state::all_resources_cache};\n\npub const TOML_PRETTY_OPTIONS: toml_pretty::Options =\n  toml_pretty::Options {\n    tab: \"  \",\n    skip_empty_string: true,\n    // Usually we do this, but has to be changed for some cases.\n    skip_empty_object: true,\n    max_inline_array_length: 30,\n    inline_array: false,\n  };\n\npub trait ToToml: KomodoResource {\n  /// Replace linked ids (server_id, build_id, etc) with the resource name.\n  fn replace_ids(_resource: &mut Resource<Self::Config, Self::Info>) {\n  }\n\n  fn edit_config_object(\n    _resource: &ResourceToml<Self::PartialConfig>,\n    config: IndexMap<String, serde_json::Value>,\n  ) -> anyhow::Result<IndexMap<String, serde_json::Value>> {\n    Ok(config)\n  }\n\n  fn push_additional(\n    _resource: ResourceToml<Self::PartialConfig>,\n    _toml: &mut String,\n  ) {\n  }\n\n  fn push_to_toml_string(\n    mut resource: ResourceToml<Self::PartialConfig>,\n    toml: &mut String,\n  ) -> anyhow::Result<()> {\n    resource.config =\n      Self::Config::default().minimize_partial(resource.config);\n\n    let mut resource_map: IndexMap<String, serde_json::Value> =\n      serde_json::from_str(&serde_json::to_string(&resource)?)?;\n    resource_map.shift_remove(\"config\");\n\n    let config = serde_json::from_str(&serde_json::to_string(\n      &resource.config,\n    )?)?;\n\n    let config = Self::edit_config_object(&resource, config)?;\n\n    toml.push_str(\n      &toml_pretty::to_string(&resource_map, TOML_PRETTY_OPTIONS)\n        .context(\"failed to serialize resource to toml\")?,\n    );\n\n    toml.push_str(&format!(\n      \"\\n[{}.config]\\n\",\n      Self::resource_type().toml_header()\n    ));\n\n    toml.push_str(\n      &toml_pretty::to_string(&config, TOML_PRETTY_OPTIONS)\n        .context(\"failed to serialize resource config to toml\")?,\n    );\n\n    Self::push_additional(resource, toml);\n\n    Ok(())\n  }\n}\n\npub fn resource_toml_to_toml_string<R: ToToml>(\n  resource: ResourceToml<R::PartialConfig>,\n) -> anyhow::Result<String> {\n  let mut toml = String::new();\n  toml\n    .push_str(&format!(\"[[{}]]\\n\", R::resource_type().toml_header()));\n  R::push_to_toml_string(resource, &mut toml)?;\n  Ok(toml)\n}\n\npub fn resource_push_to_toml<R: ToToml>(\n  mut resource: Resource<R::Config, R::Info>,\n  deploy: bool,\n  after: Vec<String>,\n  toml: &mut String,\n  all_tags: &HashMap<String, Tag>,\n) -> anyhow::Result<()> {\n  R::replace_ids(&mut resource);\n  if !toml.is_empty() {\n    toml.push_str(\"\\n\\n##\\n\\n\");\n  }\n  toml\n    .push_str(&format!(\"[[{}]]\\n\", R::resource_type().toml_header()));\n  R::push_to_toml_string(\n    convert_resource::<R>(resource, deploy, after, all_tags),\n    toml,\n  )?;\n  Ok(())\n}\n\npub fn resource_to_toml<R: ToToml>(\n  resource: Resource<R::Config, R::Info>,\n  deploy: bool,\n  after: Vec<String>,\n  all_tags: &HashMap<String, Tag>,\n) -> anyhow::Result<String> {\n  let mut toml = String::new();\n  resource_push_to_toml::<R>(\n    resource, deploy, after, &mut toml, all_tags,\n  )?;\n  Ok(toml)\n}\n\npub fn convert_resource<R: KomodoResource>(\n  resource: Resource<R::Config, R::Info>,\n  deploy: bool,\n  after: Vec<String>,\n  all_tags: &HashMap<String, Tag>,\n) -> ResourceToml<R::PartialConfig> {\n  ResourceToml {\n    name: resource.name,\n    description: resource.description,\n    template: resource.template,\n    tags: resource\n      .tags\n      .iter()\n      .filter_map(|t| all_tags.get(t).map(|t| t.name.clone()))\n      .collect(),\n    deploy,\n    after,\n    // The config still needs to be minimized.\n    // This happens in ToToml::push_to_toml\n    config: resource.config.into(),\n  }\n}\n\n// These have no linked resource ids to replace\nimpl ToToml for Alerter {}\nimpl ToToml for Server {}\nimpl ToToml for Action {}\n\nimpl ToToml for ResourceSync {\n  fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {\n    let all = all_resources_cache().load();\n    resource.config.linked_repo.clone_from(\n      all\n        .repos\n        .get(&resource.config.linked_repo)\n        .map(|r| &r.name)\n        .unwrap_or(&String::new()),\n    );\n  }\n}\n\nimpl ToToml for Stack {\n  fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {\n    let all = all_resources_cache().load();\n    resource.config.server_id.clone_from(\n      all\n        .servers\n        .get(&resource.config.server_id)\n        .map(|s| &s.name)\n        .unwrap_or(&String::new()),\n    );\n    resource.config.linked_repo.clone_from(\n      all\n        .repos\n        .get(&resource.config.linked_repo)\n        .map(|r| &r.name)\n        .unwrap_or(&String::new()),\n    );\n  }\n\n  fn edit_config_object(\n    _resource: &ResourceToml<Self::PartialConfig>,\n    config: IndexMap<String, serde_json::Value>,\n  ) -> anyhow::Result<IndexMap<String, serde_json::Value>> {\n    config\n      .into_iter()\n      .map(|(key, value)| {\n        #[allow(clippy::single_match)]\n        match key.as_str() {\n          \"server_id\" => return Ok((String::from(\"server\"), value)),\n          _ => {}\n        }\n        Ok((key, value))\n      })\n      .collect()\n  }\n}\n\nimpl ToToml for Deployment {\n  fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {\n    let all = all_resources_cache().load();\n    resource.config.server_id.clone_from(\n      all\n        .servers\n        .get(&resource.config.server_id)\n        .map(|s| &s.name)\n        .unwrap_or(&String::new()),\n    );\n    if let DeploymentImage::Build { build_id, .. } =\n      &mut resource.config.image\n    {\n      build_id.clone_from(\n        all\n          .builds\n          .get(build_id)\n          .map(|b| &b.name)\n          .unwrap_or(&String::new()),\n      );\n    }\n  }\n\n  fn edit_config_object(\n    resource: &ResourceToml<Self::PartialConfig>,\n    config: IndexMap<String, serde_json::Value>,\n  ) -> anyhow::Result<IndexMap<String, serde_json::Value>> {\n    config\n      .into_iter()\n      .map(|(key, mut value)| {\n        match key.as_str() {\n          \"server_id\" => return Ok((String::from(\"server\"), value)),\n          \"image\" => {\n            if let Some(DeploymentImage::Build { version, .. }) =\n              &resource.config.image\n            {\n              let image = value\n                .get_mut(\"params\")\n                .context(\"deployment image has no params\")?\n                .as_object_mut()\n                .context(\"deployment image params is not object\")?;\n              if let Some(build) = image.remove(\"build_id\") {\n                image.insert(String::from(\"build\"), build);\n              }\n              if version.is_none() {\n                image.remove(\"version\");\n              } else {\n                image.insert(\n                  \"version\".to_string(),\n                  serde_json::Value::String(version.to_string()),\n                );\n              }\n            }\n          }\n          _ => {}\n        }\n        Ok((key, value))\n      })\n      .collect()\n  }\n}\n\nimpl ToToml for Build {\n  fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {\n    let all = all_resources_cache().load();\n    resource.config.builder_id.clone_from(\n      all\n        .builders\n        .get(&resource.config.builder_id)\n        .map(|s| &s.name)\n        .unwrap_or(&String::new()),\n    );\n    resource.config.linked_repo.clone_from(\n      all\n        .repos\n        .get(&resource.config.linked_repo)\n        .map(|r| &r.name)\n        .unwrap_or(&String::new()),\n    );\n  }\n\n  fn edit_config_object(\n    resource: &ResourceToml<Self::PartialConfig>,\n    config: IndexMap<String, serde_json::Value>,\n  ) -> anyhow::Result<IndexMap<String, serde_json::Value>> {\n    config\n      .into_iter()\n      .map(|(key, value)| match key.as_str() {\n        \"builder_id\" => Ok((String::from(\"builder\"), value)),\n        \"version\" => {\n          match (\n            &resource.config.version,\n            resource.config.auto_increment_version,\n          ) {\n            (None, _) => Ok((key, value)),\n            (_, Some(true)) | (_, None) => {\n              // The toml shouldn't have a version attached if auto incrementing.\n              // Empty string will be filtered out in final toml.\n              Ok((key, serde_json::Value::String(String::new())))\n            }\n            (Some(version), _) => Ok((\n              key,\n              serde_json::Value::String(version.to_string()),\n            )),\n          }\n        }\n        _ => Ok((key, value)),\n      })\n      .collect()\n  }\n}\n\nimpl ToToml for Repo {\n  fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {\n    let all = all_resources_cache().load();\n    resource.config.server_id.clone_from(\n      all\n        .servers\n        .get(&resource.config.server_id)\n        .map(|s| &s.name)\n        .unwrap_or(&String::new()),\n    );\n    resource.config.builder_id.clone_from(\n      all\n        .builders\n        .get(&resource.config.builder_id)\n        .map(|s| &s.name)\n        .unwrap_or(&String::new()),\n    );\n  }\n\n  fn edit_config_object(\n    _resource: &ResourceToml<Self::PartialConfig>,\n    config: IndexMap<String, serde_json::Value>,\n  ) -> anyhow::Result<IndexMap<String, serde_json::Value>> {\n    config\n      .into_iter()\n      .map(|(key, value)| {\n        match key.as_str() {\n          \"server_id\" => return Ok((String::from(\"server\"), value)),\n          \"builder_id\" => {\n            return Ok((String::from(\"builder\"), value));\n          }\n          _ => {}\n        }\n        Ok((key, value))\n      })\n      .collect()\n  }\n}\n\nimpl ToToml for Builder {\n  fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {\n    if let BuilderConfig::Server(config) = &mut resource.config {\n      let all = all_resources_cache().load();\n      config.server_id.clone_from(\n        all\n          .servers\n          .get(&config.server_id)\n          .map(|s| &s.name)\n          .unwrap_or(&String::new()),\n      )\n    }\n  }\n\n  fn push_additional(\n    resource: ResourceToml<Self::PartialConfig>,\n    toml: &mut String,\n  ) {\n    let empty_params = match resource.config {\n      PartialBuilderConfig::Aws(config) => config.is_none(),\n      PartialBuilderConfig::Server(config) => config.is_none(),\n      PartialBuilderConfig::Url(config) => config.is_none(),\n    };\n    if empty_params {\n      // toml_pretty will remove empty map\n      // but in this case its needed to deserialize the enums.\n      toml.push_str(\"\\nparams = {}\");\n    }\n  }\n}\n\nimpl ToToml for Procedure {\n  fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {\n    let all = all_resources_cache().load();\n    for stage in &mut resource.config.stages {\n      for execution in &mut stage.executions {\n        match &mut execution.execution {\n          Execution::RunProcedure(exec) => exec.procedure.clone_from(\n            all\n              .procedures\n              .get(&exec.procedure)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::BatchRunProcedure(_exec) => {}\n          Execution::RunAction(exec) => exec.action.clone_from(\n            all\n              .actions\n              .get(&exec.action)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::BatchRunAction(_exec) => {}\n          Execution::RunBuild(exec) => exec.build.clone_from(\n            all\n              .builds\n              .get(&exec.build)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::BatchRunBuild(_exec) => {}\n          Execution::CancelBuild(exec) => exec.build.clone_from(\n            all\n              .builds\n              .get(&exec.build)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::Deploy(exec) => exec.deployment.clone_from(\n            all\n              .deployments\n              .get(&exec.deployment)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::BatchDeploy(_exec) => {}\n          Execution::PullDeployment(exec) => {\n            exec.deployment.clone_from(\n              all\n                .deployments\n                .get(&exec.deployment)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::StartDeployment(exec) => {\n            exec.deployment.clone_from(\n              all\n                .deployments\n                .get(&exec.deployment)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::RestartDeployment(exec) => {\n            exec.deployment.clone_from(\n              all\n                .deployments\n                .get(&exec.deployment)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::PauseDeployment(exec) => {\n            exec.deployment.clone_from(\n              all\n                .deployments\n                .get(&exec.deployment)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::UnpauseDeployment(exec) => {\n            exec.deployment.clone_from(\n              all\n                .deployments\n                .get(&exec.deployment)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::StopDeployment(exec) => {\n            exec.deployment.clone_from(\n              all\n                .deployments\n                .get(&exec.deployment)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::DestroyDeployment(exec) => {\n            exec.deployment.clone_from(\n              all\n                .deployments\n                .get(&exec.deployment)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::BatchDestroyDeployment(_exec) => {}\n          Execution::CloneRepo(exec) => exec.repo.clone_from(\n            all\n              .repos\n              .get(&exec.repo)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::BatchCloneRepo(_exec) => {}\n          Execution::PullRepo(exec) => exec.repo.clone_from(\n            all\n              .repos\n              .get(&exec.repo)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::BatchPullRepo(_exec) => {}\n          Execution::BuildRepo(exec) => exec.repo.clone_from(\n            all\n              .repos\n              .get(&exec.repo)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::BatchBuildRepo(_exec) => {}\n          Execution::CancelRepoBuild(exec) => exec.repo.clone_from(\n            all\n              .repos\n              .get(&exec.repo)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::StartContainer(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::RestartContainer(exec) => {\n            exec.server.clone_from(\n              all\n                .servers\n                .get(&exec.server)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::PauseContainer(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::UnpauseContainer(exec) => {\n            exec.server.clone_from(\n              all\n                .servers\n                .get(&exec.server)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::StopContainer(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::DestroyContainer(exec) => {\n            exec.server.clone_from(\n              all\n                .servers\n                .get(&exec.server)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::StartAllContainers(exec) => {\n            exec.server.clone_from(\n              all\n                .servers\n                .get(&exec.server)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::RestartAllContainers(exec) => {\n            exec.server.clone_from(\n              all\n                .servers\n                .get(&exec.server)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::PauseAllContainers(exec) => {\n            exec.server.clone_from(\n              all\n                .servers\n                .get(&exec.server)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::UnpauseAllContainers(exec) => {\n            exec.server.clone_from(\n              all\n                .servers\n                .get(&exec.server)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::StopAllContainers(exec) => {\n            exec.server.clone_from(\n              all\n                .servers\n                .get(&exec.server)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::PruneContainers(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::DeleteNetwork(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::PruneNetworks(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::DeleteImage(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::PruneImages(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::DeleteVolume(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::PruneVolumes(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::PruneDockerBuilders(exec) => {\n            exec.server.clone_from(\n              all\n                .servers\n                .get(&exec.server)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::PruneBuildx(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::PruneSystem(exec) => exec.server.clone_from(\n            all\n              .servers\n              .get(&exec.server)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::RunSync(exec) => exec.sync.clone_from(\n            all\n              .syncs\n              .get(&exec.sync)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::CommitSync(exec) => exec.sync.clone_from(\n            all\n              .syncs\n              .get(&exec.sync)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::DeployStack(exec) => exec.stack.clone_from(\n            all\n              .stacks\n              .get(&exec.stack)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::BatchDeployStack(_exec) => {}\n          Execution::DeployStackIfChanged(exec) => {\n            exec.stack.clone_from(\n              all\n                .stacks\n                .get(&exec.stack)\n                .map(|r| &r.name)\n                .unwrap_or(&String::new()),\n            )\n          }\n          Execution::BatchDeployStackIfChanged(_exec) => {}\n          Execution::PullStack(exec) => exec.stack.clone_from(\n            all\n              .stacks\n              .get(&exec.stack)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::BatchPullStack(_exec) => {}\n          Execution::StartStack(exec) => exec.stack.clone_from(\n            all\n              .stacks\n              .get(&exec.stack)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::RestartStack(exec) => exec.stack.clone_from(\n            all\n              .stacks\n              .get(&exec.stack)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::RunStackService(exec) => exec.stack.clone_from(\n            all\n              .stacks\n              .get(&exec.stack)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::PauseStack(exec) => exec.stack.clone_from(\n            all\n              .stacks\n              .get(&exec.stack)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::UnpauseStack(exec) => exec.stack.clone_from(\n            all\n              .stacks\n              .get(&exec.stack)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::StopStack(exec) => exec.stack.clone_from(\n            all\n              .stacks\n              .get(&exec.stack)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::DestroyStack(exec) => exec.stack.clone_from(\n            all\n              .stacks\n              .get(&exec.stack)\n              .map(|r| &r.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::BatchDestroyStack(_exec) => {}\n          Execution::TestAlerter(exec) => exec.alerter.clone_from(\n            all\n              .alerters\n              .get(&exec.alerter)\n              .map(|a| &a.name)\n              .unwrap_or(&String::new()),\n          ),\n          Execution::SendAlert(exec) => {\n            exec.alerters.iter_mut().for_each(|a| {\n              a.clone_from(\n                all\n                  .alerters\n                  .get(a)\n                  .map(|a| &a.name)\n                  .unwrap_or(&String::new()),\n              )\n            })\n          }\n          Execution::None(_)\n          | Execution::Sleep(_)\n          | Execution::ClearRepoCache(_)\n          | Execution::BackupCoreDatabase(_)\n          | Execution::GlobalAutoUpdate(_) => {}\n        }\n      }\n    }\n  }\n\n  fn push_to_toml_string(\n    mut resource: ResourceToml<Self::PartialConfig>,\n    toml: &mut String,\n  ) -> anyhow::Result<()> {\n    resource.config =\n      Self::Config::default().minimize_partial(resource.config);\n\n    let mut parsed: IndexMap<String, serde_json::Value> =\n      serde_json::from_str(&serde_json::to_string(&resource)?)?;\n\n    let config = parsed\n      .get_mut(\"config\")\n      .context(\"procedure has no config?\")?\n      .as_object_mut()\n      .context(\"config is not object?\")?;\n\n    let stages = config.remove(\"stages\");\n\n    toml.push_str(\n      &toml_pretty::to_string(&parsed, TOML_PRETTY_OPTIONS)\n        .context(\"failed to serialize procedures to toml\")?,\n    );\n\n    if let Some(stages) = stages {\n      let stages =\n        stages.as_array().context(\"stages is not array\")?;\n      for stage in stages {\n        toml.push_str(\"\\n\\n[[procedure.config.stage]]\\n\");\n        toml.push_str(\n          &toml_pretty::to_string(\n            stage,\n            // If the execution.params are fully missing,\n            // deserialization will fail.\n            TOML_PRETTY_OPTIONS.skip_empty_object(false),\n          )\n          .context(\"failed to serialize procedures to toml\")?,\n        );\n      }\n    }\n\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "bin/core/src/sync/user_groups.rs",
    "content": "use std::{\n  cmp::Ordering, collections::HashMap, fmt::Write, sync::OnceLock,\n};\n\nuse anyhow::Context;\nuse database::mungos::find::find_collect;\nuse formatting::{Color, bold, colored, muted};\nuse indexmap::{IndexMap, IndexSet};\nuse komodo_client::{\n  api::{\n    read::ListUserTargetPermissions,\n    write::{\n      CreateUserGroup, DeleteUserGroup, SetEveryoneUserGroup,\n      SetUsersInUserGroup, UpdatePermissionOnResourceType,\n      UpdatePermissionOnTarget,\n    },\n  },\n  entities::{\n    ResourceTarget, ResourceTargetVariant,\n    permission::{\n      PermissionLevel, PermissionLevelAndSpecifics,\n      SpecificPermission, UserTarget,\n    },\n    sync::DiffData,\n    toml::{PermissionToml, UserGroupToml},\n    update::Log,\n    user::{User, sync_user},\n    user_group::UserGroup,\n  },\n};\nuse resolver_api::Resolve;\nuse serde::Serialize;\n\nuse crate::{\n  api::{read::ReadArgs, write::WriteArgs},\n  helpers::matcher::Matcher,\n  state::{all_resources_cache, db_client},\n};\n\nuse super::toml::TOML_PRETTY_OPTIONS;\n\n/// Used to serialize user group\n#[derive(Serialize)]\nstruct BasicUserGroupToml {\n  name: String,\n  #[serde(skip_serializing_if = \"is_false\")]\n  everyone: bool,\n  #[serde(skip_serializing_if = \"Vec::is_empty\")]\n  users: Vec<String>,\n}\n\nfn is_false(b: &bool) -> bool {\n  !b\n}\n\n/// Used to serialize user group\n#[derive(Serialize)]\nstruct Permissions {\n  permissions: Vec<PermissionToml>,\n}\n\npub fn user_group_to_toml(\n  user_group: UserGroupToml,\n) -> anyhow::Result<String> {\n  // Start with the basic body\n  let basic = BasicUserGroupToml {\n    name: user_group.name,\n    everyone: user_group.everyone,\n    users: if user_group.everyone {\n      Vec::new()\n    } else {\n      user_group.users\n    },\n  };\n  let basic = toml_pretty::to_string(&basic, TOML_PRETTY_OPTIONS)\n    .context(\"failed to serialize user group to toml\")?;\n  let mut res = format!(\"[[user_group]]\\n{basic}\");\n\n  // Add \"all\" permissions\n  for (variant, PermissionLevelAndSpecifics { level, specific }) in\n    user_group.all\n  {\n    // skip 'zero' all permissions\n    if level == PermissionLevel::None && specific.is_empty() {\n      continue;\n    }\n    write!(&mut res, \"\\nall.{variant} = \")\n      .context(\"failed to serialize user group 'all' to toml\")?;\n    if specific.is_empty() {\n      res.push('\"');\n      res.push_str(level.as_ref());\n      res.push('\"');\n    } else {\n      let specific = serde_json::to_string(&specific)\n        .context(\n          \"failed to serialize user group specifics to... json?\",\n        )?\n        .replace(\",\", \", \");\n      write!(\n        &mut res,\n        \"{{ level = \\\"{level}\\\", specific = {specific} }}\"\n      )\n      .context(\n        \"failed to serialize user group 'all' with specifics to toml\",\n      )?;\n    }\n  }\n\n  // End with resource permissions array\n  if !user_group.permissions.is_empty() {\n    res.push('\\n');\n    res.push_str(\n      &toml_pretty::to_string(\n        &Permissions {\n          permissions: user_group.permissions,\n        },\n        TOML_PRETTY_OPTIONS,\n      )\n      .context(\n        \"failed to serialize user group permissions to toml\",\n      )?,\n    );\n  }\n\n  Ok(res)\n}\n\npub struct UpdateItem {\n  user_group: UserGroupToml,\n  update_users: bool,\n  update_everyone: bool,\n  all_diff:\n    IndexMap<ResourceTargetVariant, PermissionLevelAndSpecifics>,\n}\n\npub struct DeleteItem {\n  id: String,\n  name: String,\n}\n\npub async fn get_updates_for_view(\n  user_groups: Vec<UserGroupToml>,\n  delete: bool,\n) -> anyhow::Result<Vec<DiffData>> {\n  let _curr = find_collect(&db_client().user_groups, None, None)\n    .await\n    .context(\"failed to query db for UserGroups\")?;\n  let mut curr = Vec::with_capacity(_curr.capacity());\n  convert_user_groups(_curr.into_iter(), &mut curr).await?;\n  let map = curr\n    .into_iter()\n    .map(|ug| (ug.1.name.clone(), ug))\n    .collect::<HashMap<_, _>>();\n\n  let mut diffs = Vec::<DiffData>::new();\n\n  if delete {\n    for (_id, user_group) in map.values() {\n      if !user_groups.iter().any(|ug| ug.name == user_group.name) {\n        diffs.push(DiffData::Delete {\n          current: user_group_to_toml(user_group.clone())?,\n        });\n      }\n    }\n  }\n\n  for mut user_group in user_groups {\n    if user_group.everyone {\n      user_group.users.clear();\n    }\n\n    user_group\n      .permissions\n      .retain(|p| p.level > PermissionLevel::None);\n\n    user_group.permissions =\n      expand_user_group_permissions(user_group.permissions)\n        .await\n        .with_context(|| {\n          format!(\n            \"failed to expand user group {} permissions\",\n            user_group.name\n          )\n        })?;\n\n    let (_original_id, original) =\n      match map.get(&user_group.name).cloned() {\n        Some(original) => original,\n        None => {\n          diffs.push(DiffData::Create {\n            name: user_group.name.clone(),\n            proposed: user_group_to_toml(user_group.clone())?,\n          });\n          continue;\n        }\n      };\n    user_group.users.sort();\n\n    let all_diff = diff_group_all(&original.all, &user_group.all);\n\n    user_group.permissions.sort_by(sort_permissions);\n\n    let update_users = user_group.users != original.users;\n    let update_everyone = user_group.everyone != original.everyone;\n    let update_all = !all_diff.is_empty();\n    let update_permissions =\n      user_group.permissions != original.permissions;\n\n    // only add log after diff detected\n    if update_users\n      || update_everyone\n      || update_all\n      || update_permissions\n    {\n      diffs.push(DiffData::Update {\n        proposed: user_group_to_toml(user_group.clone())?,\n        current: user_group_to_toml(original.clone())?,\n      });\n    }\n  }\n\n  Ok(diffs)\n}\n\npub async fn get_updates_for_execution(\n  user_groups: Vec<UserGroupToml>,\n  delete: bool,\n) -> anyhow::Result<(\n  Vec<UserGroupToml>,\n  Vec<UpdateItem>,\n  Vec<DeleteItem>,\n)> {\n  let map = find_collect(&db_client().user_groups, None, None)\n    .await\n    .context(\"failed to query db for UserGroups\")?\n    .into_iter()\n    .map(|mut ug| {\n      if ug.everyone {\n        ug.users.clear();\n      }\n      ug.all.retain(|_, p| {\n        p.level > PermissionLevel::None || !p.specific.is_empty()\n      });\n      (ug.name.clone(), ug)\n    })\n    .collect::<HashMap<_, _>>();\n\n  let mut to_create = Vec::<UserGroupToml>::new();\n  let mut to_update = Vec::<UpdateItem>::new();\n  let mut to_delete = Vec::<DeleteItem>::new();\n\n  if delete {\n    for user_group in map.values() {\n      if !user_groups.iter().any(|ug| ug.name == user_group.name) {\n        to_delete.push(DeleteItem {\n          id: user_group.id.clone(),\n          name: user_group.name.clone(),\n        });\n      }\n    }\n  }\n\n  if user_groups.is_empty() {\n    return Ok((to_create, to_update, to_delete));\n  }\n\n  let id_to_user = find_collect(&db_client().users, None, None)\n    .await\n    .context(\"failed to query db for Users\")?\n    .into_iter()\n    .map(|user| (user.id.clone(), user))\n    .collect::<HashMap<_, _>>();\n\n  for mut user_group in user_groups {\n    if user_group.everyone {\n      user_group.users.clear();\n    }\n\n    user_group\n      .permissions\n      .retain(|p| p.level > PermissionLevel::None);\n\n    user_group.permissions =\n      expand_user_group_permissions(user_group.permissions)\n        .await\n        .with_context(|| {\n          format!(\n            \"Failed to expand user group {} permissions\",\n            user_group.name\n          )\n        })?;\n\n    let original = match map.get(&user_group.name).cloned() {\n      Some(original) => original,\n      None => {\n        to_create.push(user_group);\n        continue;\n      }\n    };\n\n    let mut original_users = original\n      .users\n      .into_iter()\n      .filter_map(|user_id| {\n        id_to_user.get(&user_id).map(|u| u.username.clone())\n      })\n      .collect::<Vec<_>>();\n\n    let all_resources = all_resources_cache().load();\n\n    let mut original_permissions = (ListUserTargetPermissions {\n      user_target: UserTarget::UserGroup(original.id),\n    })\n    .resolve(&ReadArgs {\n      user: sync_user().to_owned(),\n    })\n    .await\n    .map_err(|e| e.error)\n    .context(\"failed to query for existing UserGroup permissions\")?\n    .into_iter()\n    .filter(|p| p.level > PermissionLevel::None)\n    .map(|mut p| {\n      // replace the ids with names\n      match &mut p.resource_target {\n        ResourceTarget::System(_) => {}\n        ResourceTarget::Build(id) => {\n          *id = all_resources\n            .builds\n            .get(id)\n            .map(|b| b.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Builder(id) => {\n          *id = all_resources\n            .builders\n            .get(id)\n            .map(|b| b.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Deployment(id) => {\n          *id = all_resources\n            .deployments\n            .get(id)\n            .map(|b| b.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Server(id) => {\n          *id = all_resources\n            .servers\n            .get(id)\n            .map(|b| b.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Repo(id) => {\n          *id = all_resources\n            .repos\n            .get(id)\n            .map(|b| b.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Alerter(id) => {\n          *id = all_resources\n            .alerters\n            .get(id)\n            .map(|b| b.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Procedure(id) => {\n          *id = all_resources\n            .procedures\n            .get(id)\n            .map(|b| b.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Action(id) => {\n          *id = all_resources\n            .actions\n            .get(id)\n            .map(|b| b.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::ResourceSync(id) => {\n          *id = all_resources\n            .syncs\n            .get(id)\n            .map(|b| b.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Stack(id) => {\n          *id = all_resources\n            .stacks\n            .get(id)\n            .map(|b| b.name.clone())\n            .unwrap_or_default()\n        }\n      }\n      PermissionToml {\n        target: p.resource_target,\n        level: p.level,\n        specific: p.specific,\n      }\n    })\n    .collect::<Vec<_>>();\n\n    original_users.sort();\n    user_group.users.sort();\n\n    let all_diff = diff_group_all(&original.all, &user_group.all);\n\n    user_group.permissions.sort_by(sort_permissions);\n    original_permissions.sort_by(sort_permissions);\n\n    let update_users = user_group.users != original_users;\n    let update_everyone = user_group.everyone != original.everyone;\n\n    // Extend permissions with any existing that have no target in incoming\n    // This makes sure to set those permissions back to None.\n    let to_remove = original_permissions\n      .iter()\n      .filter(|permission| {\n        !user_group\n          .permissions\n          .iter()\n          .any(|p| p.target == permission.target)\n      })\n      .map(|permission| PermissionToml {\n        target: permission.target.clone(),\n        level: PermissionLevel::None,\n        specific: IndexSet::new(),\n      })\n      .collect::<Vec<_>>();\n    user_group.permissions.extend(to_remove);\n\n    // remove any permissions that already exist on original\n    user_group.permissions.retain(|permission| {\n      let Some(original_permission) = original_permissions\n        .iter()\n        .find(|p| p.target == permission.target)\n      else {\n        // not in original, keep it\n        return true;\n      };\n      original_permission.level != permission.level\n        || !specific_equal(\n          &original_permission.specific,\n          &permission.specific,\n        )\n    });\n\n    // only push update after diff detected\n    if update_users\n      || update_everyone\n      || !all_diff.is_empty()\n      || !user_group.permissions.is_empty()\n    {\n      to_update.push(UpdateItem {\n        user_group,\n        update_users,\n        update_everyone,\n        all_diff: all_diff\n          .into_iter()\n          .map(|(k, (_, v))| (k, v))\n          .collect(),\n      });\n    }\n  }\n\n  Ok((to_create, to_update, to_delete))\n}\n\n/// order permissions in deterministic way\nfn sort_permissions(\n  a: &PermissionToml,\n  b: &PermissionToml,\n) -> Ordering {\n  let (a_t, a_id) = a.target.extract_variant_id();\n  let (b_t, b_id) = b.target.extract_variant_id();\n  match (a_t.cmp(&b_t), a_id.cmp(b_id)) {\n    (Ordering::Greater, _) => Ordering::Greater,\n    (Ordering::Less, _) => Ordering::Less,\n    (_, Ordering::Greater) => Ordering::Greater,\n    (_, Ordering::Less) => Ordering::Less,\n    _ => Ordering::Equal,\n  }\n}\n\npub async fn run_updates(\n  to_create: Vec<UserGroupToml>,\n  to_update: Vec<UpdateItem>,\n  to_delete: Vec<DeleteItem>,\n) -> Option<Log> {\n  if to_create.is_empty()\n    && to_update.is_empty()\n    && to_delete.is_empty()\n  {\n    return None;\n  }\n\n  let mut has_error = false;\n  let mut log = String::from(\"running updates on UserGroups\");\n\n  // Create the non-existant user groups\n  for user_group in to_create {\n    // Create the user group\n    if let Err(e) = (CreateUserGroup {\n      name: user_group.name.clone(),\n    })\n    .resolve(&WriteArgs {\n      user: sync_user().to_owned(),\n    })\n    .await\n    {\n      has_error = true;\n      log.push_str(&format!(\n        \"\\n{}: failed to create user group '{}' | {:#}\",\n        colored(\"ERROR\", Color::Red),\n        bold(&user_group.name),\n        e.error\n      ));\n      continue;\n    } else {\n      log.push_str(&format!(\n        \"\\n{}: {} user group '{}'\",\n        muted(\"INFO\"),\n        colored(\"created\", Color::Green),\n        bold(&user_group.name)\n      ))\n    };\n\n    set_users(\n      user_group.name.clone(),\n      user_group.users,\n      &mut log,\n      &mut has_error,\n    )\n    .await;\n    set_everyone(\n      user_group.name.clone(),\n      user_group.everyone,\n      &mut log,\n      &mut has_error,\n    )\n    .await;\n    run_update_all(\n      user_group.name.clone(),\n      user_group.all,\n      &mut log,\n      &mut has_error,\n    )\n    .await;\n    run_update_permissions(\n      user_group.name,\n      user_group.permissions,\n      &mut log,\n      &mut has_error,\n    )\n    .await;\n  }\n\n  // Update the existing user groups\n  for UpdateItem {\n    user_group,\n    update_users,\n    update_everyone,\n    all_diff,\n  } in to_update\n  {\n    if update_users {\n      set_users(\n        user_group.name.clone(),\n        user_group.users,\n        &mut log,\n        &mut has_error,\n      )\n      .await;\n    }\n    if update_everyone {\n      set_everyone(\n        user_group.name.clone(),\n        user_group.everyone,\n        &mut log,\n        &mut has_error,\n      )\n      .await;\n    }\n    if !all_diff.is_empty() {\n      run_update_all(\n        user_group.name.clone(),\n        all_diff,\n        &mut log,\n        &mut has_error,\n      )\n      .await;\n    }\n    if !user_group.permissions.is_empty() {\n      run_update_permissions(\n        user_group.name,\n        user_group.permissions,\n        &mut log,\n        &mut has_error,\n      )\n      .await;\n    }\n  }\n\n  for user_group in to_delete {\n    if let Err(e) = (DeleteUserGroup { id: user_group.id })\n      .resolve(&WriteArgs {\n        user: sync_user().to_owned(),\n      })\n      .await\n    {\n      has_error = true;\n      log.push_str(&format!(\n        \"\\n{}: failed to delete user group '{}' | {:#}\",\n        colored(\"ERROR\", Color::Red),\n        bold(&user_group.name),\n        e.error\n      ))\n    } else {\n      log.push_str(&format!(\n        \"\\n{}: {} user group '{}'\",\n        muted(\"INFO\"),\n        colored(\"deleted\", Color::Red),\n        bold(&user_group.name)\n      ))\n    }\n  }\n\n  let stage = \"Update UserGroups\";\n  Some(if has_error {\n    Log::error(stage, log)\n  } else {\n    Log::simple(stage, log)\n  })\n}\n\nasync fn set_users(\n  user_group: String,\n  users: Vec<String>,\n  log: &mut String,\n  has_error: &mut bool,\n) {\n  if let Err(e) = (SetUsersInUserGroup {\n    user_group: user_group.clone(),\n    users,\n  })\n  .resolve(&WriteArgs {\n    user: sync_user().to_owned(),\n  })\n  .await\n  {\n    *has_error = true;\n    log.push_str(&format!(\n      \"\\n{}: failed to set users in group {} | {:#}\",\n      colored(\"ERROR\", Color::Red),\n      bold(&user_group),\n      e.error\n    ))\n  } else {\n    log.push_str(&format!(\n      \"\\n{}: {} user group '{}' users\",\n      muted(\"INFO\"),\n      colored(\"updated\", Color::Blue),\n      bold(&user_group)\n    ))\n  }\n}\n\nasync fn set_everyone(\n  user_group: String,\n  everyone: bool,\n  log: &mut String,\n  has_error: &mut bool,\n) {\n  if let Err(e) = (SetEveryoneUserGroup {\n    user_group: user_group.clone(),\n    everyone,\n  })\n  .resolve(&WriteArgs {\n    user: sync_user().to_owned(),\n  })\n  .await\n  {\n    *has_error = true;\n    log.push_str(&format!(\n      \"\\n{}: failed to set everyone for group {} | {:#}\",\n      colored(\"ERROR\", Color::Red),\n      bold(&user_group),\n      e.error\n    ))\n  } else {\n    log.push_str(&format!(\n      \"\\n{}: {} user group '{}' everyone\",\n      muted(\"INFO\"),\n      colored(\"updated\", Color::Blue),\n      bold(&user_group)\n    ))\n  }\n}\n\nasync fn run_update_all(\n  user_group: String,\n  all_diff: IndexMap<\n    ResourceTargetVariant,\n    PermissionLevelAndSpecifics,\n  >,\n  log: &mut String,\n  has_error: &mut bool,\n) {\n  for (resource_type, permission) in all_diff {\n    if let Err(e) = (UpdatePermissionOnResourceType {\n      user_target: UserTarget::UserGroup(user_group.clone()),\n      resource_type,\n      permission,\n    })\n    .resolve(&WriteArgs {\n      user: sync_user().to_owned(),\n    })\n    .await\n    {\n      *has_error = true;\n      log.push_str(&format!(\n        \"\\n{}: failed to set base permissions on {resource_type} in group {} | {:#}\",\n        colored(\"ERROR\", Color::Red),\n        bold(&user_group),\n        e.error\n      ))\n    } else {\n      log.push_str(&format!(\n        \"\\n{}: {} user group '{}' base permissions on {resource_type}\",\n        muted(\"INFO\"),\n        colored(\"updated\", Color::Blue),\n        bold(&user_group)\n      ))\n    }\n  }\n}\n\nasync fn run_update_permissions(\n  user_group: String,\n  permissions: Vec<PermissionToml>,\n  log: &mut String,\n  has_error: &mut bool,\n) {\n  for PermissionToml {\n    target,\n    level,\n    specific,\n  } in permissions\n  {\n    if let Err(e) = (UpdatePermissionOnTarget {\n      user_target: UserTarget::UserGroup(user_group.clone()),\n      resource_target: target.clone(),\n      permission: level.specifics(specific.clone()),\n    })\n    .resolve(&WriteArgs {\n      user: sync_user().to_owned(),\n    })\n    .await\n    {\n      *has_error = true;\n      log.push_str(&format!(\n        \"\\n{}: failed to set permission in group {} | target: {target:?} | {:#}\",\n        colored(\"ERROR\", Color::Red),\n        bold(&user_group),\n        e.error\n      ))\n    } else {\n      log.push_str(&format!(\n        \"\\n{}: {} user group '{}' permissions | {}: {target:?} | {}: {level} | {}: {}\",\n        muted(\"INFO\"),\n        colored(\"updated\", Color::Blue),\n        bold(&user_group),\n        muted(\"target\"),\n        muted(\"level\"),\n        muted(\"specific\"),\n        specific.into_iter().map(|s| s.into()).collect::<Vec<&'static str>>().join(\", \")\n      ))\n    }\n  }\n}\n\n/// Expands any regex defined targets into the full list\nasync fn expand_user_group_permissions(\n  permissions: Vec<PermissionToml>,\n) -> anyhow::Result<Vec<PermissionToml>> {\n  let mut expanded =\n    Vec::<PermissionToml>::with_capacity(permissions.capacity());\n  let all_resources = all_resources_cache().load();\n\n  for permission in permissions {\n    let (variant, id) = permission.target.extract_variant_id();\n    if id.is_empty() {\n      continue;\n    }\n    let matcher = Matcher::new(id)?;\n    match variant {\n      ResourceTargetVariant::Build => {\n        let permissions = all_resources\n          .builds\n          .values()\n          .filter(|resource| matcher.is_match(&resource.name))\n          .map(|resource| PermissionToml {\n            target: ResourceTarget::Build(resource.name.clone()),\n            level: permission.level,\n            specific: permission.specific.clone(),\n          });\n        expanded.extend(permissions);\n      }\n      ResourceTargetVariant::Builder => {\n        let permissions = all_resources\n          .builders\n          .values()\n          .filter(|resource| matcher.is_match(&resource.name))\n          .map(|resource| PermissionToml {\n            target: ResourceTarget::Builder(resource.name.clone()),\n            level: permission.level,\n            specific: permission.specific.clone(),\n          });\n        expanded.extend(permissions);\n      }\n      ResourceTargetVariant::Deployment => {\n        let permissions = all_resources\n          .deployments\n          .values()\n          .filter(|resource| matcher.is_match(&resource.name))\n          .map(|resource| PermissionToml {\n            target: ResourceTarget::Deployment(resource.name.clone()),\n            level: permission.level,\n            specific: permission.specific.clone(),\n          });\n        expanded.extend(permissions);\n      }\n      ResourceTargetVariant::Server => {\n        let permissions = all_resources\n          .servers\n          .values()\n          .filter(|resource| matcher.is_match(&resource.name))\n          .map(|resource| PermissionToml {\n            target: ResourceTarget::Server(resource.name.clone()),\n            level: permission.level,\n            specific: permission.specific.clone(),\n          });\n        expanded.extend(permissions);\n      }\n      ResourceTargetVariant::Repo => {\n        let permissions = all_resources\n          .repos\n          .values()\n          .filter(|resource| matcher.is_match(&resource.name))\n          .map(|resource| PermissionToml {\n            target: ResourceTarget::Repo(resource.name.clone()),\n            level: permission.level,\n            specific: permission.specific.clone(),\n          });\n        expanded.extend(permissions);\n      }\n      ResourceTargetVariant::Alerter => {\n        let permissions = all_resources\n          .alerters\n          .values()\n          .filter(|resource| matcher.is_match(&resource.name))\n          .map(|resource| PermissionToml {\n            target: ResourceTarget::Alerter(resource.name.clone()),\n            level: permission.level,\n            specific: permission.specific.clone(),\n          });\n        expanded.extend(permissions);\n      }\n      ResourceTargetVariant::Procedure => {\n        let permissions = all_resources\n          .procedures\n          .values()\n          .filter(|resource| matcher.is_match(&resource.name))\n          .map(|resource| PermissionToml {\n            target: ResourceTarget::Procedure(resource.name.clone()),\n            level: permission.level,\n            specific: permission.specific.clone(),\n          });\n        expanded.extend(permissions);\n      }\n      ResourceTargetVariant::Action => {\n        let permissions = all_resources\n          .actions\n          .values()\n          .filter(|resource| matcher.is_match(&resource.name))\n          .map(|resource| PermissionToml {\n            target: ResourceTarget::Action(resource.name.clone()),\n            level: permission.level,\n            specific: permission.specific.clone(),\n          });\n        expanded.extend(permissions);\n      }\n      ResourceTargetVariant::ResourceSync => {\n        let permissions = all_resources\n          .syncs\n          .values()\n          .filter(|resource| matcher.is_match(&resource.name))\n          .map(|resource| PermissionToml {\n            target: ResourceTarget::ResourceSync(\n              resource.name.clone(),\n            ),\n            level: permission.level,\n            specific: permission.specific.clone(),\n          });\n        expanded.extend(permissions);\n      }\n      ResourceTargetVariant::Stack => {\n        let permissions = all_resources\n          .stacks\n          .values()\n          .filter(|resource| matcher.is_match(&resource.name))\n          .map(|resource| PermissionToml {\n            target: ResourceTarget::Stack(resource.name.clone()),\n            level: permission.level,\n            specific: permission.specific.clone(),\n          });\n        expanded.extend(permissions);\n      }\n      ResourceTargetVariant::System => {}\n    }\n  }\n\n  Ok(expanded)\n}\n\ntype AllDiff = IndexMap<\n  ResourceTargetVariant,\n  (PermissionLevelAndSpecifics, PermissionLevelAndSpecifics),\n>;\n\nfn default_permission() -> &'static PermissionLevelAndSpecifics {\n  static DEFAULT_PERMISSION: OnceLock<PermissionLevelAndSpecifics> =\n    OnceLock::new();\n  DEFAULT_PERMISSION.get_or_init(Default::default)\n}\n\n/// diffs user_group.all\nfn diff_group_all(\n  original: &IndexMap<\n    ResourceTargetVariant,\n    PermissionLevelAndSpecifics,\n  >,\n  incoming: &IndexMap<\n    ResourceTargetVariant,\n    PermissionLevelAndSpecifics,\n  >,\n) -> AllDiff {\n  let mut to_update = IndexMap::new();\n\n  // need to compare both forward and backward because either hashmap could be sparse.\n\n  // forward direction\n  for (variant, permission) in incoming {\n    let original_permission =\n      original.get(variant).unwrap_or(default_permission());\n    if permission.level != original_permission.level\n      || !specific_equal(\n        &original_permission.specific,\n        &permission.specific,\n      )\n    {\n      to_update.insert(\n        *variant,\n        (original_permission.clone(), permission.clone()),\n      );\n    }\n  }\n\n  // backward direction\n  for (variant, permission) in original {\n    let incoming_permission =\n      incoming.get(variant).unwrap_or(default_permission());\n    if permission.level != incoming_permission.level\n      || !specific_equal(\n        &incoming_permission.specific,\n        &permission.specific,\n      )\n    {\n      to_update.insert(\n        *variant,\n        (permission.clone(), incoming_permission.clone()),\n      );\n    }\n  }\n\n  to_update\n}\n\nfn specific_equal(\n  a: &IndexSet<SpecificPermission>,\n  b: &IndexSet<SpecificPermission>,\n) -> bool {\n  for item in a {\n    if !b.contains(item) {\n      return false;\n    }\n  }\n  for item in b {\n    if !a.contains(item) {\n      return false;\n    }\n  }\n  true\n}\n\npub async fn convert_user_groups(\n  user_groups: impl Iterator<Item = UserGroup>,\n  res: &mut Vec<(String, UserGroupToml)>,\n) -> anyhow::Result<()> {\n  let db = db_client();\n\n  let usernames = find_collect(&db.users, None, None)\n    .await?\n    .into_iter()\n    .map(|user| (user.id, user.username))\n    .collect::<HashMap<_, _>>();\n\n  let all = all_resources_cache().load();\n\n  for mut user_group in user_groups {\n    user_group.all.retain(|_, p| {\n      p.level > PermissionLevel::None || !p.specific.is_empty()\n    });\n\n    // this method is admin only, but we already know user can see user group if above does not return Err\n    let mut permissions = (ListUserTargetPermissions {\n      user_target: UserTarget::UserGroup(user_group.id.clone()),\n    })\n    .resolve(&ReadArgs {\n      user: User {\n        admin: true,\n        ..Default::default()\n      },\n    })\n    .await\n    .map_err(|e| e.error)?\n    .into_iter()\n    .filter(|permission| permission.level > PermissionLevel::None)\n    .map(|mut permission| {\n      match &mut permission.resource_target {\n        ResourceTarget::Build(id) => {\n          *id = all\n            .builds\n            .get(id)\n            .map(|r| r.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Builder(id) => {\n          *id = all\n            .builders\n            .get(id)\n            .map(|r| r.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Deployment(id) => {\n          *id = all\n            .deployments\n            .get(id)\n            .map(|r| r.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Server(id) => {\n          *id = all\n            .servers\n            .get(id)\n            .map(|r| r.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Repo(id) => {\n          *id = all\n            .repos\n            .get(id)\n            .map(|r| r.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Alerter(id) => {\n          *id = all\n            .alerters\n            .get(id)\n            .map(|r| r.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Procedure(id) => {\n          *id = all\n            .procedures\n            .get(id)\n            .map(|r| r.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Action(id) => {\n          *id = all\n            .actions\n            .get(id)\n            .map(|r| r.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::ResourceSync(id) => {\n          *id = all\n            .syncs\n            .get(id)\n            .map(|r| r.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::Stack(id) => {\n          *id = all\n            .stacks\n            .get(id)\n            .map(|r| r.name.clone())\n            .unwrap_or_default()\n        }\n        ResourceTarget::System(_) => {}\n      }\n      PermissionToml {\n        target: permission.resource_target,\n        level: permission.level,\n        specific: permission.specific,\n      }\n    })\n    .collect::<Vec<_>>();\n\n    let mut users = if user_group.everyone {\n      Vec::new()\n    } else {\n      user_group\n        .users\n        .into_iter()\n        .filter_map(|user_id| usernames.get(&user_id).cloned())\n        .collect::<Vec<_>>()\n    };\n\n    permissions.sort_by(sort_permissions);\n    users.sort();\n\n    res.push((\n      user_group.id,\n      UserGroupToml {\n        name: user_group.name,\n        everyone: user_group.everyone,\n        all: user_group.all,\n        users,\n        permissions,\n      },\n    ));\n  }\n  Ok(())\n}\n"
  },
  {
    "path": "bin/core/src/sync/variables.rs",
    "content": "use std::collections::HashMap;\n\nuse anyhow::Context;\nuse database::mungos::find::find_collect;\nuse formatting::{Color, bold, colored, muted};\nuse komodo_client::{\n  api::write::*,\n  entities::{\n    sync::DiffData, update::Log, user::sync_user, variable::Variable,\n  },\n};\nuse resolver_api::Resolve;\n\nuse crate::{api::write::WriteArgs, state::db_client};\n\nuse super::toml::TOML_PRETTY_OPTIONS;\n\npub fn variable_to_toml(\n  variable: &Variable,\n) -> anyhow::Result<String> {\n  let inner = toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS)\n    .context(\"failed to serialize variable to toml\")?;\n  Ok(format!(\"[[variable]]\\n{inner}\"))\n}\n\npub struct ToUpdateItem {\n  pub variable: Variable,\n  pub update_value: bool,\n  pub update_description: bool,\n  pub update_is_secret: bool,\n}\n\npub async fn get_updates_for_view(\n  variables: &[Variable],\n  delete: bool,\n) -> anyhow::Result<Vec<DiffData>> {\n  let map = find_collect(&db_client().variables, None, None)\n    .await\n    .context(\"failed to query db for variables\")?\n    .into_iter()\n    .map(|v| (v.name.clone(), v))\n    .collect::<HashMap<_, _>>();\n\n  let mut diffs = Vec::<DiffData>::new();\n\n  if delete {\n    for variable in map.values() {\n      if !variables.iter().any(|v| v.name == variable.name) {\n        diffs.push(DiffData::Delete {\n          current: variable_to_toml(variable)?,\n        });\n      }\n    }\n  }\n\n  for variable in variables {\n    match map.get(&variable.name) {\n      Some(original) => {\n        if original.value == variable.value\n          && original.description == variable.description\n        {\n          continue;\n        }\n        diffs.push(DiffData::Update {\n          proposed: variable_to_toml(variable)?,\n          current: variable_to_toml(original)?,\n        });\n      }\n      None => {\n        diffs.push(DiffData::Create {\n          name: variable.name.clone(),\n          proposed: variable_to_toml(variable)?,\n        });\n      }\n    }\n  }\n\n  Ok(diffs)\n}\n\npub async fn get_updates_for_execution(\n  variables: Vec<Variable>,\n  delete: bool,\n) -> anyhow::Result<(Vec<Variable>, Vec<ToUpdateItem>, Vec<String>)> {\n  let map = find_collect(&db_client().variables, None, None)\n    .await\n    .context(\"failed to query db for variables\")?\n    .into_iter()\n    .map(|v| (v.name.clone(), v))\n    .collect::<HashMap<_, _>>();\n\n  let mut to_create = Vec::<Variable>::new();\n  let mut to_update = Vec::<ToUpdateItem>::new();\n  let mut to_delete = Vec::<String>::new();\n\n  if delete {\n    for variable in map.values() {\n      if !variables.iter().any(|v| v.name == variable.name) {\n        to_delete.push(variable.name.clone());\n      }\n    }\n  }\n\n  for variable in variables {\n    match map.get(&variable.name) {\n      Some(original) => {\n        let item = ToUpdateItem {\n          update_value: original.value != variable.value,\n          update_description: original.description\n            != variable.description,\n          update_is_secret: original.is_secret != variable.is_secret,\n          variable,\n        };\n        if !item.update_value\n          && !item.update_description\n          && !item.update_is_secret\n        {\n          continue;\n        }\n\n        to_update.push(item);\n      }\n      None => to_create.push(variable),\n    }\n  }\n\n  Ok((to_create, to_update, to_delete))\n}\n\npub async fn run_updates(\n  to_create: Vec<Variable>,\n  to_update: Vec<ToUpdateItem>,\n  to_delete: Vec<String>,\n) -> Option<Log> {\n  if to_create.is_empty()\n    && to_update.is_empty()\n    && to_delete.is_empty()\n  {\n    return None;\n  }\n\n  let mut has_error = false;\n  let mut log = String::from(\"running updates on Variables\");\n\n  for variable in to_create {\n    if let Err(e) = (CreateVariable {\n      name: variable.name.clone(),\n      value: variable.value,\n      description: variable.description,\n      is_secret: variable.is_secret,\n    })\n    .resolve(&WriteArgs {\n      user: sync_user().to_owned(),\n    })\n    .await\n    {\n      has_error = true;\n      log.push_str(&format!(\n        \"\\n{}: failed to create variable '{}' | {:#}\",\n        colored(\"ERROR\", Color::Red),\n        bold(&variable.name),\n        e.error\n      ));\n    } else {\n      log.push_str(&format!(\n        \"\\n{}: {} variable '{}'\",\n        muted(\"INFO\"),\n        colored(\"created\", Color::Green),\n        bold(&variable.name)\n      ))\n    };\n  }\n\n  for ToUpdateItem {\n    variable,\n    update_value,\n    update_description,\n    update_is_secret,\n  } in to_update\n  {\n    if update_value {\n      if let Err(e) = (UpdateVariableValue {\n        name: variable.name.clone(),\n        value: variable.value,\n      })\n      .resolve(&WriteArgs {\n        user: sync_user().to_owned(),\n      })\n      .await\n      {\n        has_error = true;\n        log.push_str(&format!(\n          \"\\n{}: failed to update variable value for '{}' | {:#}\",\n          colored(\"ERROR\", Color::Red),\n          bold(&variable.name),\n          e.error\n        ))\n      } else {\n        log.push_str(&format!(\n          \"\\n{}: {} variable '{}' value\",\n          muted(\"INFO\"),\n          colored(\"updated\", Color::Blue),\n          bold(&variable.name)\n        ))\n      };\n    }\n    if update_description {\n      if let Err(e) = (UpdateVariableDescription {\n        name: variable.name.clone(),\n        description: variable.description,\n      })\n      .resolve(&WriteArgs {\n        user: sync_user().to_owned(),\n      })\n      .await\n      {\n        has_error = true;\n        log.push_str(&format!(\n          \"\\n{}: failed to update variable description for '{}' | {:#}\",\n          colored(\"ERROR\", Color::Red),\n          bold(&variable.name),\n          e.error\n        ))\n      } else {\n        log.push_str(&format!(\n          \"\\n{}: {} variable '{}' description\",\n          muted(\"INFO\"),\n          colored(\"updated\", Color::Blue),\n          bold(&variable.name)\n        ))\n      };\n    }\n    if update_is_secret {\n      if let Err(e) = (UpdateVariableIsSecret {\n        name: variable.name.clone(),\n        is_secret: variable.is_secret,\n      })\n      .resolve(&WriteArgs {\n        user: sync_user().to_owned(),\n      })\n      .await\n      {\n        has_error = true;\n        log.push_str(&format!(\n          \"\\n{}: failed to update variable is secret for '{}' | {:#}\",\n          colored(\"ERROR\", Color::Red),\n          bold(&variable.name),\n          e.error,\n        ))\n      } else {\n        log.push_str(&format!(\n          \"\\n{}: {} variable '{}' is secret\",\n          muted(\"INFO\"),\n          colored(\"updated\", Color::Blue),\n          bold(&variable.name)\n        ))\n      };\n    }\n  }\n\n  for variable in to_delete {\n    if let Err(e) = (DeleteVariable {\n      name: variable.clone(),\n    })\n    .resolve(&WriteArgs {\n      user: sync_user().to_owned(),\n    })\n    .await\n    {\n      has_error = true;\n      log.push_str(&format!(\n        \"\\n{}: failed to delete variable '{}' | {:#}\",\n        colored(\"ERROR\", Color::Red),\n        bold(&variable),\n        e.error\n      ))\n    } else {\n      log.push_str(&format!(\n        \"\\n{}: {} variable '{}'\",\n        muted(\"INFO\"),\n        colored(\"deleted\", Color::Red),\n        bold(&variable)\n      ))\n    }\n  }\n\n  let stage = \"Update Variables\";\n  Some(if has_error {\n    Log::error(stage, log)\n  } else {\n    Log::simple(stage, log)\n  })\n}\n"
  },
  {
    "path": "bin/core/src/sync/view.rs",
    "content": "use std::collections::HashMap;\n\nuse anyhow::Context;\nuse database::mungos::find::find_collect;\nuse komodo_client::entities::{\n  ResourceTargetVariant,\n  sync::{DiffData, ResourceDiff},\n  tag::Tag,\n  toml::ResourceToml,\n};\nuse partial_derive2::MaybeNone;\n\nuse super::ResourceSyncTrait;\n\n#[allow(clippy::too_many_arguments)]\npub async fn push_updates_for_view<Resource: ResourceSyncTrait>(\n  resources: Vec<ResourceToml<Resource::PartialConfig>>,\n  delete: bool,\n  match_resource_type: Option<ResourceTargetVariant>,\n  match_resources: Option<&[String]>,\n  id_to_tags: &HashMap<String, Tag>,\n  match_tags: &[String],\n  diffs: &mut Vec<ResourceDiff>,\n) -> anyhow::Result<()> {\n  let current_map = find_collect(Resource::coll(), None, None)\n    .await\n    .context(\"failed to get resources from db\")?\n    .into_iter()\n    .filter(|r| {\n      Resource::include_resource(\n        &r.name,\n        &r.config,\n        match_resource_type,\n        match_resources,\n        &r.tags,\n        id_to_tags,\n        match_tags,\n      )\n    })\n    .map(|r| (r.name.clone(), r))\n    .collect::<HashMap<_, _>>();\n\n  let resources = resources\n    .into_iter()\n    .filter(|r| {\n      Resource::include_resource_partial(\n        &r.name,\n        &r.config,\n        match_resource_type,\n        match_resources,\n        &r.tags,\n        id_to_tags,\n        match_tags,\n      )\n    })\n    .collect::<Vec<_>>();\n\n  if delete {\n    for current_resource in current_map.values() {\n      if !resources.iter().any(|r| r.name == current_resource.name) {\n        diffs.push(ResourceDiff {\n          target: Resource::resource_target(\n            current_resource.id.clone(),\n          ),\n          data: DiffData::Delete {\n            current: super::toml::resource_to_toml::<Resource>(\n              current_resource.clone(),\n              false,\n              vec![],\n              id_to_tags,\n            )?,\n          },\n        });\n      }\n    }\n  }\n\n  for mut proposed_resource in resources {\n    match current_map.get(&proposed_resource.name) {\n      Some(current_resource) => {\n        // First merge toml resource config (partial) onto default resource config.\n        // Makes sure things that aren't defined in toml (come through as None) actually get removed.\n        let propsed_config: Resource::Config =\n          proposed_resource.config.into();\n        proposed_resource.config = propsed_config.into();\n\n        Resource::validate_partial_config(\n          &mut proposed_resource.config,\n        );\n\n        let proposed = super::toml::resource_toml_to_toml_string::<\n          Resource,\n        >(proposed_resource.clone())?;\n\n        let mut diff = Resource::get_diff(\n          current_resource.config.clone(),\n          proposed_resource.config,\n        )?;\n\n        Resource::validate_diff(&mut diff);\n\n        let current_tags = current_resource\n          .tags\n          .iter()\n          .filter_map(|id| id_to_tags.get(id).map(|t| t.name.clone()))\n          .collect::<Vec<_>>();\n\n        // Only proceed if there are any fields to update,\n        // or a change to tags / description\n        if diff.is_none()\n          && proposed_resource.description\n            == current_resource.description\n          && proposed_resource.tags == current_tags\n        {\n          continue;\n        }\n\n        diffs.push(ResourceDiff {\n          target: Resource::resource_target(\n            current_resource.id.clone(),\n          ),\n          data: DiffData::Update {\n            current: super::toml::resource_to_toml::<Resource>(\n              current_resource.clone(),\n              proposed_resource.deploy,\n              proposed_resource.after,\n              id_to_tags,\n            )?,\n            proposed,\n          },\n        });\n      }\n      None => {\n        diffs.push(ResourceDiff {\n          // resources to Create don't have ids yet.\n          target: Resource::resource_target(String::new()),\n\n          data: DiffData::Create {\n            name: proposed_resource.name.clone(),\n            proposed: super::toml::resource_toml_to_toml_string::<\n              Resource,\n            >(proposed_resource)?,\n          },\n        });\n      }\n    }\n  }\n\n  Ok(())\n}\n"
  },
  {
    "path": "bin/core/src/ts_client.rs",
    "content": "use anyhow::{Context, anyhow};\nuse axum::{\n  Router,\n  extract::Path,\n  http::{HeaderMap, HeaderValue},\n  routing::get,\n};\nuse reqwest::StatusCode;\nuse serde::Deserialize;\nuse serror::AddStatusCodeError;\nuse tokio::fs;\n\nuse crate::config::core_config;\n\npub fn router() -> Router {\n  Router::new().route(\"/{path}\", get(serve_client_file))\n}\n\nconst ALLOWED_FILES: &[&str] = &[\n  \"lib.js\",\n  \"lib.d.ts\",\n  \"types.js\",\n  \"types.d.ts\",\n  \"responses.js\",\n  \"responses.d.ts\",\n  \"terminal.js\",\n  \"terminal.d.ts\",\n];\n\n#[derive(Deserialize)]\nstruct FilePath {\n  path: String,\n}\n\n#[axum::debug_handler]\nasync fn serve_client_file(\n  Path(FilePath { path }): Path<FilePath>,\n) -> serror::Result<(HeaderMap, String)> {\n  if !ALLOWED_FILES.contains(&path.as_str()) {\n    return Err(\n      anyhow!(\"File {path} not found.\")\n        .status_code(StatusCode::NOT_FOUND),\n    );\n  }\n\n  let contents = fs::read_to_string(format!(\n    \"{}/client/{path}\",\n    core_config().frontend_path\n  ))\n  .await\n  .with_context(|| format!(\"Failed to read file: {path}\"))?;\n\n  let mut headers = HeaderMap::new();\n\n  if path.ends_with(\".js\") {\n    headers.insert(\n      \"X-TypeScript-Types\",\n      HeaderValue::from_str(&format!(\n        \"/client/{}\",\n        path.replace(\".js\", \".d.ts\")\n      ))\n      .context(\"?? Invalid Header Value\")?,\n    );\n  }\n\n  Ok((headers, contents))\n}\n"
  },
  {
    "path": "bin/core/src/ws/container.rs",
    "content": "use axum::{\n  extract::{Query, WebSocketUpgrade, ws::Message},\n  response::IntoResponse,\n};\nuse futures::SinkExt;\nuse komodo_client::{\n  api::terminal::ConnectContainerExecQuery,\n  entities::{permission::PermissionLevel, server::Server},\n};\n\nuse crate::permission::get_check_permissions;\n\n#[instrument(name = \"ConnectContainerExec\", skip(ws))]\npub async fn terminal(\n  Query(ConnectContainerExecQuery {\n    server,\n    container,\n    shell,\n  }): Query<ConnectContainerExecQuery>,\n  ws: WebSocketUpgrade,\n) -> impl IntoResponse {\n  ws.on_upgrade(|socket| async move {\n    let Some((mut client_socket, user)) =\n      super::ws_login(socket).await\n    else {\n      return;\n    };\n\n    let server = match get_check_permissions::<Server>(\n      &server,\n      &user,\n      PermissionLevel::Read.terminal(),\n    )\n    .await\n    {\n      Ok(server) => server,\n      Err(e) => {\n        debug!(\"could not get server | {e:#}\");\n        let _ = client_socket\n          .send(Message::text(format!(\"ERROR: {e:#}\")))\n          .await;\n        let _ = client_socket.close().await;\n        return;\n      }\n    };\n\n    super::handle_container_terminal(\n      client_socket,\n      &server,\n      container,\n      shell,\n    )\n    .await\n  })\n}\n"
  },
  {
    "path": "bin/core/src/ws/deployment.rs",
    "content": "use axum::{\n  extract::{Query, WebSocketUpgrade, ws::Message},\n  response::IntoResponse,\n};\nuse futures::SinkExt;\nuse komodo_client::{\n  api::terminal::ConnectDeploymentExecQuery,\n  entities::{\n    deployment::Deployment, permission::PermissionLevel,\n    server::Server,\n  },\n};\n\nuse crate::{permission::get_check_permissions, resource::get};\n\n#[instrument(name = \"ConnectDeploymentExec\", skip(ws))]\npub async fn terminal(\n  Query(ConnectDeploymentExecQuery { deployment, shell }): Query<\n    ConnectDeploymentExecQuery,\n  >,\n  ws: WebSocketUpgrade,\n) -> impl IntoResponse {\n  ws.on_upgrade(|socket| async move {\n    let Some((mut client_socket, user)) =\n      super::ws_login(socket).await\n    else {\n      return;\n    };\n\n    let deployment = match get_check_permissions::<Deployment>(\n      &deployment,\n      &user,\n      PermissionLevel::Read.terminal(),\n    )\n    .await\n    {\n      Ok(deployment) => deployment,\n      Err(e) => {\n        debug!(\"could not get deployment | {e:#}\");\n        let _ = client_socket\n          .send(Message::text(format!(\"ERROR: {e:#}\")))\n          .await;\n        let _ = client_socket.close().await;\n        return;\n      }\n    };\n\n    let server =\n      match get::<Server>(&deployment.config.server_id).await {\n        Ok(server) => server,\n        Err(e) => {\n          debug!(\"could not get server | {e:#}\");\n          let _ = client_socket\n            .send(Message::text(format!(\"ERROR: {e:#}\")))\n            .await;\n          let _ = client_socket.close().await;\n          return;\n        }\n      };\n\n    super::handle_container_terminal(\n      client_socket,\n      &server,\n      deployment.name,\n      shell,\n    )\n    .await\n  })\n}\n"
  },
  {
    "path": "bin/core/src/ws/mod.rs",
    "content": "use crate::{\n  auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},\n  helpers::query::get_user,\n};\nuse anyhow::anyhow;\nuse axum::{\n  Router,\n  extract::ws::{CloseFrame, Message, Utf8Bytes, WebSocket},\n  routing::get,\n};\nuse futures::{SinkExt, StreamExt};\nuse komodo_client::{\n  entities::{server::Server, user::User},\n  ws::WsLoginMessage,\n};\nuse tokio::net::TcpStream;\nuse tokio_tungstenite::{\n  MaybeTlsStream, WebSocketStream, tungstenite,\n};\nuse tokio_util::sync::CancellationToken;\n\nmod container;\nmod deployment;\nmod stack;\nmod terminal;\nmod update;\n\npub fn router() -> Router {\n  Router::new()\n    .route(\"/update\", get(update::handler))\n    .route(\"/terminal\", get(terminal::handler))\n    .route(\"/container/terminal\", get(container::terminal))\n    .route(\"/deployment/terminal\", get(deployment::terminal))\n    .route(\"/stack/terminal\", get(stack::terminal))\n}\n\n#[instrument(level = \"debug\")]\nasync fn ws_login(\n  mut socket: WebSocket,\n) -> Option<(WebSocket, User)> {\n  let login_msg = match socket.recv().await {\n    Some(Ok(Message::Text(login_msg))) => {\n      LoginMessage::Ok(login_msg.to_string())\n    }\n    Some(Ok(msg)) => {\n      LoginMessage::Err(format!(\"invalid login message: {msg:?}\"))\n    }\n    Some(Err(e)) => {\n      LoginMessage::Err(format!(\"failed to get login message: {e:?}\"))\n    }\n    None => {\n      LoginMessage::Err(\"failed to get login message\".to_string())\n    }\n  };\n  let login_msg = match login_msg {\n    LoginMessage::Ok(login_msg) => login_msg,\n    LoginMessage::Err(msg) => {\n      let _ = socket.send(Message::text(msg)).await;\n      let _ = socket.close().await;\n      return None;\n    }\n  };\n  match WsLoginMessage::from_json_str(&login_msg) {\n    // Login using a jwt\n    Ok(WsLoginMessage::Jwt { jwt }) => {\n      match auth_jwt_check_enabled(&jwt).await {\n        Ok(user) => {\n          let _ = socket.send(Message::text(\"LOGGED_IN\")).await;\n          Some((socket, user))\n        }\n        Err(e) => {\n          let _ = socket\n            .send(Message::text(format!(\n              \"failed to authenticate user using jwt | {e:#}\"\n            )))\n            .await;\n          let _ = socket.close().await;\n          None\n        }\n      }\n    }\n    // login using api keys\n    Ok(WsLoginMessage::ApiKeys { key, secret }) => {\n      match auth_api_key_check_enabled(&key, &secret).await {\n        Ok(user) => {\n          let _ = socket.send(Message::text(\"LOGGED_IN\")).await;\n          Some((socket, user))\n        }\n        Err(e) => {\n          let _ = socket\n            .send(Message::text(format!(\n              \"failed to authenticate user using api keys | {e:#}\"\n            )))\n            .await;\n          let _ = socket.close().await;\n          None\n        }\n      }\n    }\n    Err(e) => {\n      let _ = socket\n        .send(Message::text(format!(\n          \"failed to parse login message: {e:#}\"\n        )))\n        .await;\n      let _ = socket.close().await;\n      None\n    }\n  }\n}\n\nenum LoginMessage {\n  /// The text message\n  Ok(String),\n  /// The err message\n  Err(String),\n}\n\n#[instrument(level = \"debug\")]\nasync fn check_user_valid(user_id: &str) -> anyhow::Result<User> {\n  let user = get_user(user_id).await?;\n  if !user.enabled {\n    return Err(anyhow!(\"user not enabled\"));\n  }\n  Ok(user)\n}\n\nasync fn handle_container_terminal(\n  mut client_socket: WebSocket,\n  server: &Server,\n  container: String,\n  shell: String,\n) {\n  let periphery = match crate::helpers::periphery_client(server) {\n    Ok(periphery) => periphery,\n    Err(e) => {\n      debug!(\"couldn't get periphery | {e:#}\");\n      let _ = client_socket\n        .send(Message::text(format!(\"ERROR: {e:#}\")))\n        .await;\n      let _ = client_socket.close().await;\n      return;\n    }\n  };\n\n  trace!(\"connecting to periphery container exec websocket\");\n\n  let periphery_socket = match periphery\n    .connect_container_exec(container, shell)\n    .await\n  {\n    Ok(ws) => ws,\n    Err(e) => {\n      debug!(\n        \"Failed connect to periphery container exec websocket | {e:#}\"\n      );\n      let _ = client_socket\n        .send(Message::text(format!(\"ERROR: {e:#}\")))\n        .await;\n      let _ = client_socket.close().await;\n      return;\n    }\n  };\n\n  trace!(\"connected to periphery container exec websocket\");\n\n  core_periphery_forward_ws(client_socket, periphery_socket).await\n}\n\nasync fn core_periphery_forward_ws(\n  client_socket: axum::extract::ws::WebSocket,\n  periphery_socket: WebSocketStream<MaybeTlsStream<TcpStream>>,\n) {\n  let (mut periphery_send, mut periphery_receive) =\n    periphery_socket.split();\n  let (mut core_send, mut core_receive) = client_socket.split();\n  let cancel = CancellationToken::new();\n\n  trace!(\"starting ws exchange\");\n\n  let core_to_periphery = async {\n    loop {\n      let res = tokio::select! {\n        res = core_receive.next() => res,\n        _ = cancel.cancelled() => {\n          trace!(\"core to periphery read: cancelled from inside\");\n          break;\n        }\n      };\n      match res {\n        Some(Ok(msg)) => {\n          if let Err(e) =\n            periphery_send.send(axum_to_tungstenite(msg)).await\n          {\n            debug!(\"Failed to send terminal message | {e:?}\",);\n            cancel.cancel();\n            break;\n          };\n        }\n        Some(Err(_e)) => {\n          cancel.cancel();\n          break;\n        }\n        None => {\n          cancel.cancel();\n          break;\n        }\n      }\n    }\n  };\n\n  let periphery_to_core = async {\n    loop {\n      let res = tokio::select! {\n        res = periphery_receive.next() => res,\n        _ = cancel.cancelled() => {\n          trace!(\"periphery to core read: cancelled from inside\");\n          break;\n        }\n      };\n      match res {\n        Some(Ok(msg)) => {\n          if let Err(e) =\n            core_send.send(tungstenite_to_axum(msg)).await\n          {\n            debug!(\"{e:?}\");\n            cancel.cancel();\n            break;\n          };\n        }\n        Some(Err(e)) => {\n          let _ = core_send\n              .send(Message::text(format!(\n                \"ERROR: Failed to receive message from periphery | {e:?}\"\n              )))\n              .await;\n          cancel.cancel();\n          break;\n        }\n        None => {\n          let _ = core_send.send(Message::text(\"STREAM EOF\")).await;\n          cancel.cancel();\n          break;\n        }\n      }\n    }\n  };\n\n  tokio::join!(core_to_periphery, periphery_to_core);\n}\n\nfn axum_to_tungstenite(msg: Message) -> tungstenite::Message {\n  match msg {\n    Message::Text(text) => tungstenite::Message::Text(\n      // TODO: improve this conversion cost from axum ws library\n      tungstenite::Utf8Bytes::from(text.to_string()),\n    ),\n    Message::Binary(bytes) => tungstenite::Message::Binary(bytes),\n    Message::Ping(bytes) => tungstenite::Message::Ping(bytes),\n    Message::Pong(bytes) => tungstenite::Message::Pong(bytes),\n    Message::Close(close_frame) => {\n      tungstenite::Message::Close(close_frame.map(|cf| {\n        tungstenite::protocol::CloseFrame {\n          code: cf.code.into(),\n          reason: tungstenite::Utf8Bytes::from(cf.reason.to_string()),\n        }\n      }))\n    }\n  }\n}\n\nfn tungstenite_to_axum(msg: tungstenite::Message) -> Message {\n  match msg {\n    tungstenite::Message::Text(text) => {\n      Message::Text(Utf8Bytes::from(text.to_string()))\n    }\n    tungstenite::Message::Binary(bytes) => Message::Binary(bytes),\n    tungstenite::Message::Ping(bytes) => Message::Ping(bytes),\n    tungstenite::Message::Pong(bytes) => Message::Pong(bytes),\n    tungstenite::Message::Close(close_frame) => {\n      Message::Close(close_frame.map(|cf| CloseFrame {\n        code: cf.code.into(),\n        reason: Utf8Bytes::from(cf.reason.to_string()),\n      }))\n    }\n    tungstenite::Message::Frame(_) => {\n      unreachable!()\n    }\n  }\n}\n"
  },
  {
    "path": "bin/core/src/ws/stack.rs",
    "content": "use axum::{\n  extract::{Query, WebSocketUpgrade, ws::Message},\n  response::IntoResponse,\n};\nuse futures::SinkExt;\nuse komodo_client::{\n  api::terminal::ConnectStackExecQuery,\n  entities::{\n    permission::PermissionLevel, server::Server, stack::Stack,\n  },\n};\n\nuse crate::{\n  permission::get_check_permissions, resource::get,\n  state::stack_status_cache,\n};\n\n#[instrument(name = \"ConnectStackExec\", skip(ws))]\npub async fn terminal(\n  Query(ConnectStackExecQuery {\n    stack,\n    service,\n    shell,\n  }): Query<ConnectStackExecQuery>,\n  ws: WebSocketUpgrade,\n) -> impl IntoResponse {\n  ws.on_upgrade(|socket| async move {\n    let Some((mut client_socket, user)) =\n      super::ws_login(socket).await\n    else {\n      return;\n    };\n\n    let stack = match get_check_permissions::<Stack>(\n      &stack,\n      &user,\n      PermissionLevel::Read.terminal(),\n    )\n    .await\n    {\n      Ok(stack) => stack,\n      Err(e) => {\n        debug!(\"could not get stack | {e:#}\");\n        let _ = client_socket\n          .send(Message::text(format!(\"ERROR: {e:#}\")))\n          .await;\n        let _ = client_socket.close().await;\n        return;\n      }\n    };\n\n    let server = match get::<Server>(&stack.config.server_id).await {\n      Ok(server) => server,\n      Err(e) => {\n        debug!(\"could not get server | {e:#}\");\n        let _ = client_socket\n          .send(Message::text(format!(\"ERROR: {e:#}\")))\n          .await;\n        let _ = client_socket.close().await;\n        return;\n      }\n    };\n\n    let Some(status) = stack_status_cache().get(&stack.id).await\n    else {\n      debug!(\"could not get stack status\");\n      let _ = client_socket\n        .send(Message::text(String::from(\n          \"ERROR: could not get stack status\",\n        )))\n        .await;\n      let _ = client_socket.close().await;\n      return;\n    };\n\n    let container = match status\n      .curr\n      .services\n      .iter()\n      .find(|s| s.service == service)\n      .map(|s| s.container.as_ref())\n    {\n      Some(Some(container)) => container.name.clone(),\n      Some(None) => {\n        let _ = client_socket\n          .send(Message::text(format!(\n            \"ERROR: Service {service} container could not be found\"\n          )))\n          .await;\n        let _ = client_socket.close().await;\n        return;\n      }\n      None => {\n        let _ = client_socket\n          .send(Message::text(format!(\n            \"ERROR: Service {service} could not be found\"\n          )))\n          .await;\n        let _ = client_socket.close().await;\n        return;\n      }\n    };\n\n    super::handle_container_terminal(\n      client_socket,\n      &server,\n      container,\n      shell,\n    )\n    .await\n  })\n}\n"
  },
  {
    "path": "bin/core/src/ws/terminal.rs",
    "content": "use axum::{\n  extract::{Query, WebSocketUpgrade, ws::Message},\n  response::IntoResponse,\n};\nuse futures::SinkExt;\nuse komodo_client::{\n  api::terminal::ConnectTerminalQuery,\n  entities::{permission::PermissionLevel, server::Server},\n};\n\nuse crate::{\n  helpers::periphery_client, permission::get_check_permissions,\n  ws::core_periphery_forward_ws,\n};\n\n#[instrument(name = \"ConnectTerminal\", skip(ws))]\npub async fn handler(\n  Query(ConnectTerminalQuery { server, terminal }): Query<\n    ConnectTerminalQuery,\n  >,\n  ws: WebSocketUpgrade,\n) -> impl IntoResponse {\n  ws.on_upgrade(|socket| async move {\n    let Some((mut client_socket, user)) =\n      super::ws_login(socket).await\n    else {\n      return;\n    };\n\n    let server = match get_check_permissions::<Server>(\n      &server,\n      &user,\n      PermissionLevel::Read.terminal(),\n    )\n    .await\n    {\n      Ok(server) => server,\n      Err(e) => {\n        debug!(\"could not get server | {e:#}\");\n        let _ = client_socket\n          .send(Message::text(format!(\"ERROR: {e:#}\")))\n          .await;\n        let _ = client_socket.close().await;\n        return;\n      }\n    };\n\n    let periphery = match periphery_client(&server) {\n      Ok(periphery) => periphery,\n      Err(e) => {\n        debug!(\"couldn't get periphery | {e:#}\");\n        let _ = client_socket\n          .send(Message::text(format!(\"ERROR: {e:#}\")))\n          .await;\n        let _ = client_socket.close().await;\n        return;\n      }\n    };\n\n    trace!(\"connecting to periphery terminal websocket\");\n\n    let periphery_socket =\n      match periphery.connect_terminal(terminal).await {\n        Ok(ws) => ws,\n        Err(e) => {\n          debug!(\"Failed connect to periphery terminal | {e:#}\");\n          let _ = client_socket\n            .send(Message::text(format!(\"ERROR: {e:#}\")))\n            .await;\n          let _ = client_socket.close().await;\n          return;\n        }\n      };\n\n    trace!(\"connected to periphery terminal websocket\");\n\n    core_periphery_forward_ws(client_socket, periphery_socket).await\n  })\n}\n"
  },
  {
    "path": "bin/core/src/ws/update.rs",
    "content": "use anyhow::anyhow;\nuse axum::{\n  extract::{WebSocketUpgrade, ws::Message},\n  response::IntoResponse,\n};\nuse futures::{SinkExt, StreamExt};\nuse komodo_client::entities::{\n  ResourceTarget, permission::PermissionLevel, user::User,\n};\nuse serde_json::json;\nuse serror::serialize_error;\nuse tokio::select;\nuse tokio_util::sync::CancellationToken;\n\nuse crate::helpers::{\n  channel::update_channel, query::get_user_permission_on_target,\n};\n\n#[instrument(level = \"debug\")]\npub async fn handler(ws: WebSocketUpgrade) -> impl IntoResponse {\n  // get a reveiver for internal update messages.\n  let mut receiver = update_channel().receiver.resubscribe();\n\n  // handle http -> ws updgrade\n  ws.on_upgrade(|socket| async move {\n    let Some((socket, user)) = super::ws_login(socket).await else {\n      return\n    };\n\n    let (mut ws_sender, mut ws_reciever) = socket.split();\n\n    let cancel = CancellationToken::new();\n    let cancel_clone = cancel.clone();\n\n    tokio::spawn(async move {\n      loop {\n        // poll for updates off the receiver / await cancel.\n        let update = select! {\n          _ = cancel_clone.cancelled() => break,\n          update = receiver.recv() => {update.expect(\"failed to recv update msg\")}\n        };\n\n        // before sending every update, verify user is still valid.\n        // kill the connection is user if found to be invalid.\n        let user = super::check_user_valid(&user.id).await;\n        let user = match user {\n          Err(e) => {\n            let _ = ws_sender\n              .send(Message::text(json!({ \"type\": \"INVALID_USER\", \"msg\": serialize_error(&e) }).to_string()))\n              .await;\n            let _ = ws_sender.close().await;\n            return;\n          },\n          Ok(user) => user,\n        };\n\n        // Only send if user has permission on the target resource.\n        if user_can_see_update(&user, &update.target).await.is_ok() {\n          let _ = ws_sender\n            .send(Message::text(serde_json::to_string(&update).unwrap()))\n            .await;\n        }\n      }\n    });\n\n    // Handle messages from the client.\n    // After login, only handles close message.\n    while let Some(msg) = ws_reciever.next().await {\n      match msg {\n        Ok(msg) => {\n          if let Message::Close(_) = msg {\n            cancel.cancel();\n            return;\n          }\n        }\n        Err(_) => {\n          cancel.cancel();\n          return;\n        }\n      }\n    }\n    })\n}\n\n#[instrument(level = \"debug\")]\nasync fn user_can_see_update(\n  user: &User,\n  update_target: &ResourceTarget,\n) -> anyhow::Result<()> {\n  if user.admin {\n    return Ok(());\n  }\n  let permission =\n    get_user_permission_on_target(user, update_target).await?;\n  if permission.level > PermissionLevel::None {\n    Ok(())\n  } else {\n    Err(anyhow!(\n      \"user does not have permissions on {update_target:?}\"\n    ))\n  }\n}\n"
  },
  {
    "path": "bin/core/starship.toml",
    "content": "## This is used to customize the shell prompt in Periphery container for Terminals\n\n\"$schema\" = 'https://starship.rs/config-schema.json'\n\nadd_newline = true\n\nformat = \"$time$hostname$container$memory_usage$all\"\n\n[character]\nsuccess_symbol = \"[❯](bright-blue bold)\"\nerror_symbol = \"[❯](bright-red bold)\"\n\n[package]\ndisabled = true\n\n[time]\nformat = \"[❯$time](white dimmed) \"\ntime_format = \"%l:%M %p\"\nutc_time_offset = '-5'\ndisabled = true\n\n[username]\nformat = \"[❯ $user]($style) \"\nstyle_user = \"bright-green\"\nshow_always = true\n\n[hostname]\nformat = \"[❯ $hostname]($style) \"\nstyle = \"bright-blue\"\nssh_only = false\n\n[directory]\nformat = \"[❯ $path]($style)[$read_only]($read_only_style) \"\nstyle = \"bright-cyan\"\n\n[git_branch]\nformat = \"[❯ $symbol$branch(:$remote_branch)]($style) \"\nstyle = \"bright-purple\"\n\n[git_status]\nstyle = \"bright-purple\"\n\n[rust]\nformat = \"[❯ $symbol($version )]($style)\"\nsymbol = \"rustc \"\nstyle = \"bright-red\"\n\n[nodejs]\nformat = \"[❯ $symbol($version )]($style)\"\nsymbol = \"nodejs \"\nstyle = \"bright-red\"\n\n[memory_usage]\nformat = \"[❯ mem ${ram} ${ram_pct}]($style) \"\nthreshold = -1\nstyle = \"white\"\n\n[cmd_duration]\nformat = \"[❯ $duration]($style)\"\nstyle = \"bright-yellow\"\n\n[container]\nformat = \"[❯ 🦎 core container ]($style)\"\nstyle = \"bright-green\"\n\n[aws]\ndisabled = true\n"
  },
  {
    "path": "bin/periphery/Cargo.toml",
    "content": "[package]\nname = \"komodo_periphery\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\n\n[[bin]]\nname = \"periphery\"\npath = \"src/main.rs\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n# local\nkomodo_client.workspace = true\nperiphery_client.workspace = true\nenvironment_file.workspace = true\nenvironment.workspace = true\ninterpolate.workspace = true\nformatting.workspace = true\nresponse.workspace = true\ncommand.workspace = true\nconfig.workspace = true\nlogger.workspace = true\ncache.workspace = true\ngit.workspace = true\n# mogh\nserror = { workspace = true, features = [\"axum\"] }\nasync_timing_util.workspace = true\nderive_variants.workspace = true\nresolver_api.workspace = true\nrun_command.workspace = true\n# external\npin-project-lite.workspace = true\ntokio-stream.workspace = true\nportable-pty.workspace = true\naxum-server.workspace = true\nserde_json.workspace = true\nserde_yaml_ng.workspace = true\ntokio-util.workspace = true\narc-swap.workspace = true\ncolored.workspace = true\nfutures.workspace = true\ntracing.workspace = true\nbollard.workspace = true\nsysinfo.workspace = true\ndotenvy.workspace = true\nanyhow.workspace = true\nrustls.workspace = true\ntokio.workspace = true\nserde.workspace = true\nbytes.workspace = true\naxum.workspace = true\nclap.workspace = true\nenvy.workspace = true\nuuid.workspace = true\nrand.workspace = true\nshell-escape.workspace = true"
  },
  {
    "path": "bin/periphery/aio.Dockerfile",
    "content": "## All in one, multi stage compile + runtime Docker build for your architecture.\n\nFROM rust:1.89.0-bullseye AS builder\nRUN cargo install cargo-strip\n\nWORKDIR /builder\nCOPY Cargo.toml Cargo.lock ./\nCOPY ./lib ./lib\nCOPY ./client/core/rs ./client/core/rs\nCOPY ./client/periphery ./client/periphery\nCOPY ./bin/periphery ./bin/periphery\n\n# Compile app\nRUN cargo build -p komodo_periphery --release && cargo strip\n\n# Final Image\nFROM debian:bullseye-slim\n\nCOPY ./bin/periphery/starship.toml /starship.toml\nCOPY ./bin/periphery/debian-deps.sh .\nRUN sh ./debian-deps.sh && rm ./debian-deps.sh\n\nCOPY --from=builder /builder/target/release/periphery /usr/local/bin/periphery\n\nEXPOSE 8120\n\nCMD [ \"periphery\" ]\n\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo Periphery\"\nLABEL org.opencontainers.image.licenses=GPL-3.0\n"
  },
  {
    "path": "bin/periphery/debian-deps.sh",
    "content": "#!/bin/bash\n\n## Periphery deps installer\n\napt-get update\napt-get install -y git curl wget ca-certificates\n\ninstall -m 0755 -d /etc/apt/keyrings\ncurl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc\nchmod a+r /etc/apt/keyrings/docker.asc\n\necho \\\n  \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \\\n  $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable\" | \\\n  tee /etc/apt/sources.list.d/docker.list > /dev/null\n\napt-get update\n\n# apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\napt-get install -y docker-ce-cli docker-buildx-plugin docker-compose-plugin\n\nrm -rf /var/lib/apt/lists/*\n\n# Starship prompt\ncurl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin\necho 'export STARSHIP_CONFIG=/starship.toml' >> /root/.bashrc\necho 'eval \"$(starship init bash)\"' >> /root/.bashrc\n\n"
  },
  {
    "path": "bin/periphery/multi-arch.Dockerfile",
    "content": "## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).\n## Sets up the necessary runtime container dependencies for Komodo Periphery.\n## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.\n\nARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest\nARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64\nARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64\n\n# This is required to work with COPY --from\nFROM ${X86_64_BINARIES} AS x86_64\nFROM ${AARCH64_BINARIES} AS aarch64\n\nFROM debian:bullseye-slim\n\nCOPY ./bin/periphery/starship.toml /starship.toml\nCOPY ./bin/periphery/debian-deps.sh .\nRUN sh ./debian-deps.sh && rm ./debian-deps.sh\n\nWORKDIR /app\n\n## Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.\nCOPY --from=x86_64 /periphery /app/arch/linux/amd64\nCOPY --from=aarch64 /periphery /app/arch/linux/arm64\n\nARG TARGETPLATFORM\nRUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/periphery && rm -r /app/arch\n\nEXPOSE 8120\n\nCMD [ \"periphery\" ]\n\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo Periphery\"\nLABEL org.opencontainers.image.licenses=GPL-3.0\n"
  },
  {
    "path": "bin/periphery/single-arch.Dockerfile",
    "content": "## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).\n## Sets up the necessary runtime container dependencies for Komodo Periphery.\n\nARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest\n\n# This is required to work with COPY --from\nFROM ${BINARIES_IMAGE} AS binaries\n\nFROM debian:bullseye-slim\n\nCOPY ./bin/periphery/starship.toml /starship.toml\nCOPY ./bin/periphery/debian-deps.sh .\nRUN sh ./debian-deps.sh && rm ./debian-deps.sh\n\nCOPY --from=binaries /periphery /usr/local/bin/periphery\n\nEXPOSE 8120\n\nCMD [ \"periphery\" ]\n\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo Periphery\"\nLABEL org.opencontainers.image.licenses=GPL-3.0\n"
  },
  {
    "path": "bin/periphery/src/api/build.rs",
    "content": "use std::{\n  collections::{HashMap, HashSet},\n  path::PathBuf,\n};\n\nuse anyhow::{Context, anyhow};\nuse command::{\n  run_komodo_command, run_komodo_command_with_sanitization,\n};\nuse formatting::format_serror;\nuse interpolate::Interpolator;\nuse komodo_client::entities::{\n  EnvironmentVar, all_logs_success,\n  build::{Build, BuildConfig},\n  environment_vars_from_str, optional_string,\n  to_path_compatible_name,\n  update::Log,\n};\nuse periphery_client::api::build::{\n  self, GetDockerfileContentsOnHost,\n  GetDockerfileContentsOnHostResponse, PruneBuilders, PruneBuildx,\n  WriteDockerfileContentsToHost,\n};\nuse resolver_api::Resolve;\nuse tokio::fs;\n\nuse crate::{\n  build::{parse_build_args, parse_secret_args, write_dockerfile},\n  config::periphery_config,\n  docker::docker_login,\n  helpers::{parse_extra_args, parse_labels},\n};\n\nimpl Resolve<super::Args> for GetDockerfileContentsOnHost {\n  #[instrument(name = \"GetDockerfileContentsOnHost\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<GetDockerfileContentsOnHostResponse> {\n    let GetDockerfileContentsOnHost {\n      name,\n      build_path,\n      dockerfile_path,\n    } = self;\n\n    let root = periphery_config()\n      .build_dir()\n      .join(to_path_compatible_name(&name));\n    let build_dir =\n      root.join(&build_path).components().collect::<PathBuf>();\n\n    if !build_dir.exists() {\n      fs::create_dir_all(&build_dir)\n        .await\n        .context(\"Failed to initialize build directory\")?;\n    }\n\n    let full_path = build_dir\n      .join(&dockerfile_path)\n      .components()\n      .collect::<PathBuf>();\n\n    let contents =\n      fs::read_to_string(&full_path).await.with_context(|| {\n        format!(\"Failed to read dockerfile contents at {full_path:?}\")\n      })?;\n\n    Ok(GetDockerfileContentsOnHostResponse {\n      contents,\n      path: full_path.display().to_string(),\n    })\n  }\n}\n\nimpl Resolve<super::Args> for WriteDockerfileContentsToHost {\n  #[instrument(\n    name = \"WriteDockerfileContentsToHost\",\n    skip_all,\n    fields(\n      stack = &self.name,\n      build_path = &self.build_path,\n      dockerfile_path = &self.dockerfile_path,\n    )\n  )]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let WriteDockerfileContentsToHost {\n      name,\n      build_path,\n      dockerfile_path,\n      contents,\n    } = self;\n    let full_path = periphery_config()\n      .build_dir()\n      .join(to_path_compatible_name(&name))\n      .join(&build_path)\n      .join(dockerfile_path)\n      .components()\n      .collect::<PathBuf>();\n    // Ensure parent directory exists\n    if let Some(parent) = full_path.parent()\n      && !parent.exists()\n    {\n      tokio::fs::create_dir_all(parent)\n        .await\n        .with_context(|| format!(\"Failed to initialize dockerfile parent directory {parent:?}\"))?;\n    }\n    fs::write(&full_path, contents).await.with_context(|| {\n      format!(\"Failed to write dockerfile contents to {full_path:?}\")\n    })?;\n    Ok(Log::simple(\n      \"Write dockerfile to host\",\n      format!(\"dockerfile contents written to {full_path:?}\"),\n    ))\n  }\n}\n\nimpl Resolve<super::Args> for build::Build {\n  #[instrument(name = \"Build\", skip_all, fields(build = self.build.name.to_string()))]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<Log>> {\n    let build::Build {\n      mut build,\n      repo: linked_repo,\n      registry_tokens,\n      mut replacers,\n      commit_hash,\n      additional_tags,\n    } = self;\n\n    let mut logs = Vec::new();\n\n    // Periphery side interpolation\n    let mut interpolator =\n      Interpolator::new(None, &periphery_config().secrets);\n    interpolator\n      .interpolate_build(&mut build)?\n      .push_logs(&mut logs);\n\n    replacers.extend(interpolator.secret_replacers);\n\n    let Build {\n      name,\n      config:\n        BuildConfig {\n          build_path,\n          dockerfile_path,\n          build_args,\n          secret_args,\n          labels,\n          extra_args,\n          use_buildx,\n          image_registry,\n          repo,\n          files_on_host,\n          dockerfile,\n          pre_build,\n          ..\n        },\n      ..\n    } = &build;\n\n    if !*files_on_host\n      && repo.is_empty()\n      && linked_repo.is_none()\n      && dockerfile.is_empty()\n    {\n      return Err(anyhow!(\"Build must be files on host mode, have a repo attached, or have dockerfile contents set to build\").into());\n    }\n\n    let registry_tokens = registry_tokens\n      .iter()\n      .map(|(domain, account, token)| {\n        ((domain.as_str(), account.as_str()), token.as_str())\n      })\n      .collect::<HashMap<_, _>>();\n\n    // Maybe docker login\n    let mut should_push = false;\n    for (domain, account) in image_registry\n      .iter()\n      .map(|r| (r.domain.as_str(), r.account.as_str()))\n      // This ensures uniqueness / prevents redundant logins\n      .collect::<HashSet<_>>()\n    {\n      match docker_login(\n        domain,\n        account,\n        registry_tokens.get(&(domain, account)).copied(),\n      )\n      .await\n      {\n        Ok(logged_in) if logged_in => should_push = true,\n        Ok(_) => {}\n        Err(e) => {\n          logs.push(Log::error(\n            \"Docker Login\",\n            format_serror(\n              &e.context(\"failed to login to docker registry\").into(),\n            ),\n          ));\n          return Ok(logs);\n        }\n      };\n    }\n\n    let build_path = if let Some(repo) = &linked_repo {\n      periphery_config()\n        .repo_dir()\n        .join(to_path_compatible_name(&repo.name))\n        .join(build_path)\n    } else {\n      periphery_config()\n        .build_dir()\n        .join(to_path_compatible_name(name))\n        .join(build_path)\n    }\n    .components()\n    .collect::<PathBuf>();\n\n    let dockerfile_path = optional_string(dockerfile_path)\n      .unwrap_or(\"Dockerfile\".to_owned());\n\n    // Write UI defined Dockerfile to host\n    if !*files_on_host\n      && repo.is_empty()\n      && linked_repo.is_none()\n      && !dockerfile.is_empty()\n    {\n      write_dockerfile(\n        &build_path,\n        &dockerfile_path,\n        dockerfile,\n        &mut logs,\n      )\n      .await;\n      if !all_logs_success(&logs) {\n        return Ok(logs);\n      }\n    };\n\n    // Pre Build\n    if !pre_build.is_none() {\n      let pre_build_path = build_path.join(&pre_build.path);\n      if let Some(log) = run_komodo_command_with_sanitization(\n        \"Pre Build\",\n        pre_build_path.as_path(),\n        &pre_build.command,\n        true,\n        &replacers,\n      )\n      .await\n      {\n        let success = log.success;\n        logs.push(log);\n        if !success {\n          return Ok(logs);\n        }\n      }\n    }\n\n    // Get command parts\n\n    // Add VERSION to build args (if not already there)\n    let mut build_args = environment_vars_from_str(build_args)\n      .context(\"Invalid build_args\")?;\n    if !build_args.iter().any(|a| a.variable == \"VERSION\") {\n      build_args.push(EnvironmentVar {\n        variable: String::from(\"VERSION\"),\n        value: build.config.version.to_string(),\n      });\n    }\n    let build_args = parse_build_args(&build_args);\n\n    let secret_args = environment_vars_from_str(secret_args)\n      .context(\"Invalid secret_args\")?;\n    let command_secret_args =\n      parse_secret_args(&secret_args, &build_path).await?;\n\n    let labels = parse_labels(\n      &environment_vars_from_str(labels).context(\"Invalid labels\")?,\n    );\n\n    let extra_args = parse_extra_args(extra_args);\n\n    let buildx = if *use_buildx { \" buildx\" } else { \"\" };\n\n    let image_tags = build\n      .get_image_tags_as_arg(commit_hash.as_deref(), &additional_tags)\n      .context(\"Failed to parse image tags into command\")?;\n\n    let maybe_push = if should_push { \" --push\" } else { \"\" };\n\n    // Construct command\n    let command = format!(\n      \"docker{buildx} build{build_args}{command_secret_args}{extra_args}{labels}{image_tags}{maybe_push} -f {dockerfile_path} .\",\n    );\n\n    if let Some(build_log) = run_komodo_command_with_sanitization(\n      \"Docker Build\",\n      build_path.as_ref(),\n      command,\n      false,\n      &replacers,\n    )\n    .await\n    {\n      logs.push(build_log);\n    };\n\n    Ok(logs)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for PruneBuilders {\n  #[instrument(name = \"PruneBuilders\", skip_all)]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let command = String::from(\"docker builder prune -a -f\");\n    Ok(run_komodo_command(\"Prune Builders\", None, command).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for PruneBuildx {\n  #[instrument(name = \"PruneBuildx\", skip_all)]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let command = String::from(\"docker buildx prune -a -f\");\n    Ok(run_komodo_command(\"Prune Buildx\", None, command).await)\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/api/compose.rs",
    "content": "use anyhow::{Context, anyhow};\nuse command::{\n  run_komodo_command, run_komodo_command_with_sanitization,\n};\nuse formatting::format_serror;\nuse git::write_commit_file;\nuse interpolate::Interpolator;\nuse komodo_client::entities::{\n  FileContents, RepoExecutionResponse, all_logs_success,\n  stack::{\n    ComposeFile, ComposeProject, ComposeService,\n    ComposeServiceDeploy, StackRemoteFileContents, StackServiceNames,\n  },\n  to_path_compatible_name,\n  update::Log,\n};\nuse periphery_client::api::compose::*;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse shell_escape::unix::escape;\nuse std::{borrow::Cow, path::PathBuf};\nuse tokio::fs;\n\nuse crate::{\n  compose::{\n    docker_compose, env_file_args, pull_or_clone_stack,\n    up::{maybe_login_registry, validate_files},\n    write::write_stack,\n  },\n  config::periphery_config,\n  helpers::{log_grep, parse_extra_args},\n};\n\nimpl Resolve<super::Args> for ListComposeProjects {\n  #[instrument(name = \"ComposeInfo\", level = \"debug\", skip_all)]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<ComposeProject>> {\n    let docker_compose = docker_compose();\n    let res = run_komodo_command(\n      \"List Projects\",\n      None,\n      format!(\"{docker_compose} ls --all --format json\"),\n    )\n    .await;\n\n    if !res.success {\n      return Err(\n        anyhow!(\"{}\", res.combined())\n          .context(format!(\n            \"failed to list compose projects using {docker_compose} ls\"\n          ))\n          .into(),\n      );\n    }\n\n    let res =\n      serde_json::from_str::<Vec<DockerComposeLsItem>>(&res.stdout)\n        .with_context(|| res.stdout.clone())\n        .with_context(|| {\n          format!(\n            \"failed to parse '{docker_compose} ls' response to json\"\n          )\n        })?\n        .into_iter()\n        .filter(|item| !item.name.is_empty())\n        .map(|item| ComposeProject {\n          name: item.name,\n          status: item.status,\n          compose_files: item\n            .config_files\n            .split(',')\n            .map(str::to_string)\n            .collect(),\n        })\n        .collect();\n\n    Ok(res)\n  }\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct DockerComposeLsItem {\n  #[serde(default, alias = \"Name\")]\n  pub name: String,\n  #[serde(alias = \"Status\")]\n  pub status: Option<String>,\n  /// Comma seperated list of paths\n  #[serde(default, alias = \"ConfigFiles\")]\n  pub config_files: String,\n}\n\n//\n\nimpl Resolve<super::Args> for GetComposeLog {\n  #[instrument(name = \"GetComposeLog\", level = \"debug\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let GetComposeLog {\n      project,\n      services,\n      tail,\n      timestamps,\n    } = self;\n    let docker_compose = docker_compose();\n    let timestamps = if timestamps {\n      \" --timestamps\"\n    } else {\n      Default::default()\n    };\n    let command = format!(\n      \"{docker_compose} -p {project} logs --tail {tail}{timestamps} {}\",\n      services.join(\" \")\n    );\n    Ok(run_komodo_command(\"get stack log\", None, command).await)\n  }\n}\n\nimpl Resolve<super::Args> for GetComposeLogSearch {\n  #[instrument(name = \"GetComposeLogSearch\", level = \"debug\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let GetComposeLogSearch {\n      project,\n      services,\n      terms,\n      combinator,\n      invert,\n      timestamps,\n    } = self;\n    let docker_compose = docker_compose();\n    let grep = log_grep(&terms, combinator, invert);\n    let timestamps = if timestamps {\n      \" --timestamps\"\n    } else {\n      Default::default()\n    };\n    let command = format!(\n      \"{docker_compose} -p {project} logs --tail 5000{timestamps} {} 2>&1 | {grep}\",\n      services.join(\" \")\n    );\n    Ok(run_komodo_command(\"Get stack log grep\", None, command).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for GetComposeContentsOnHost {\n  #[instrument(name = \"GetComposeContentsOnHost\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<GetComposeContentsOnHostResponse> {\n    let GetComposeContentsOnHost {\n      name,\n      run_directory,\n      file_paths,\n    } = self;\n    let root = periphery_config()\n      .stack_dir()\n      .join(to_path_compatible_name(&name));\n    let run_directory =\n      root.join(&run_directory).components().collect::<PathBuf>();\n\n    if !run_directory.exists() {\n      fs::create_dir_all(&run_directory)\n        .await\n        .context(\"Failed to initialize run directory\")?;\n    }\n\n    let mut res = GetComposeContentsOnHostResponse::default();\n\n    for file in file_paths {\n      let full_path = run_directory\n        .join(&file.path)\n        .components()\n        .collect::<PathBuf>();\n      match fs::read_to_string(&full_path).await.with_context(|| {\n        format!(\n          \"Failed to read compose file contents at {full_path:?}\"\n        )\n      }) {\n        Ok(contents) => {\n          // The path we store here has to be the same as incoming file path in the array,\n          // in order for WriteComposeContentsToHost to write to the correct path.\n          res.contents.push(StackRemoteFileContents {\n            path: file.path,\n            contents,\n            services: file.services,\n            requires: file.requires,\n          });\n        }\n        Err(e) => {\n          res.errors.push(FileContents {\n            path: file.path,\n            contents: format_serror(&e.into()),\n          });\n        }\n      }\n    }\n\n    Ok(res)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for WriteComposeContentsToHost {\n  #[instrument(\n    name = \"WriteComposeContentsToHost\",\n    skip_all,\n    fields(\n      stack = &self.name,\n      run_directory = &self.run_directory,\n      file_path = &self.file_path,\n    )\n  )]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let WriteComposeContentsToHost {\n      name,\n      run_directory,\n      file_path,\n      contents,\n    } = self;\n    let file_path = periphery_config()\n      .stack_dir()\n      .join(to_path_compatible_name(&name))\n      .join(&run_directory)\n      .join(file_path)\n      .components()\n      .collect::<PathBuf>();\n    // Ensure parent directory exists\n    if let Some(parent) = file_path.parent() {\n      fs::create_dir_all(&parent)\n        .await\n        .with_context(|| format!(\"Failed to initialize compose file parent directory {parent:?}\"))?;\n    }\n    fs::write(&file_path, contents).await.with_context(|| {\n      format!(\n        \"Failed to write compose file contents to {file_path:?}\"\n      )\n    })?;\n    Ok(Log::simple(\n      \"Write contents to host\",\n      format!(\"File contents written to {file_path:?}\"),\n    ))\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for WriteCommitComposeContents {\n  #[instrument(\n    name = \"WriteCommitComposeContents\",\n    skip_all,\n    fields(\n      stack = &self.stack.name,\n      username = &self.username,\n      file_path = &self.file_path,\n    )\n  )]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<RepoExecutionResponse> {\n    let WriteCommitComposeContents {\n      stack,\n      repo,\n      username,\n      file_path,\n      contents,\n      git_token,\n    } = self;\n\n    let root =\n      pull_or_clone_stack(&stack, repo.as_ref(), git_token).await?;\n\n    let file_path = stack\n      .config\n      .run_directory\n      .parse::<PathBuf>()\n      .context(\"Run directory is not a valid path\")?\n      .join(&file_path);\n\n    let msg = if let Some(username) = username {\n      format!(\"{username}: Write Compose File\")\n    } else {\n      \"Write Compose File\".to_string()\n    };\n\n    write_commit_file(\n      &msg,\n      &root,\n      &file_path,\n      &contents,\n      &stack.config.branch,\n    )\n    .await\n    .map_err(Into::into)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for ComposePull {\n  #[instrument(\n    name = \"ComposePull\",\n    skip_all,\n    fields(\n      stack = &self.stack.name,\n      services = format!(\"{:?}\", self.services),\n    )\n  )]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<ComposePullResponse> {\n    let ComposePull {\n      mut stack,\n      repo,\n      services,\n      git_token,\n      registry_token,\n      mut replacers,\n    } = self;\n\n    let mut res = ComposePullResponse::default();\n\n    let mut interpolator =\n      Interpolator::new(None, &periphery_config().secrets);\n    // Only interpolate Stack. Repo interpolation will be handled\n    // by the CloneRepo / PullOrCloneRepo call.\n    interpolator\n      .interpolate_stack(&mut stack)?\n      .push_logs(&mut res.logs);\n    replacers.extend(interpolator.secret_replacers);\n\n    let (run_directory, env_file_path) = match write_stack(\n      &stack,\n      repo.as_ref(),\n      git_token,\n      replacers.clone(),\n      &mut res,\n    )\n    .await\n    {\n      Ok(res) => res,\n      Err(e) => {\n        res\n          .logs\n          .push(Log::error(\"Write Stack\", format_serror(&e.into())));\n        return Ok(res);\n      }\n    };\n\n    // Canonicalize the path to ensure it exists, and is the cleanest path to the run directory.\n    let run_directory = run_directory.canonicalize().context(\n      \"Failed to validate run directory on host after stack write (canonicalize error)\",\n    )?;\n\n    let file_paths = stack\n      .all_file_paths()\n      .into_iter()\n      .map(|path| {\n        (\n          // This will remove any intermediate uneeded '/./' in the path\n          run_directory.join(&path).components().collect::<PathBuf>(),\n          path,\n        )\n      })\n      .collect::<Vec<_>>();\n\n    // Validate files\n    for (full_path, path) in &file_paths {\n      if !full_path.exists() {\n        return Err(anyhow!(\"Missing compose file at {path}\").into());\n      }\n    }\n\n    maybe_login_registry(&stack, registry_token, &mut res.logs).await;\n    if !all_logs_success(&res.logs) {\n      return Ok(res);\n    }\n\n    let docker_compose = docker_compose();\n\n    let service_args = if services.is_empty() {\n      String::new()\n    } else {\n      format!(\" {}\", services.join(\" \"))\n    };\n\n    let file_args = stack.compose_file_paths().join(\" -f \");\n\n    let env_file_args = env_file_args(\n      env_file_path,\n      &stack.config.additional_env_files,\n    )?;\n\n    let project_name = stack.project_name(false);\n\n    let log = run_komodo_command(\n      \"Compose Pull\",\n      run_directory.as_ref(),\n      format!(\n        \"{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull{service_args}\",\n      ),\n    )\n    .await;\n\n    res.logs.push(log);\n\n    Ok(res)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for ComposeUp {\n  #[instrument(\n    name = \"ComposeUp\",\n    skip_all,\n    fields(\n      stack = &self.stack.name,\n      services = format!(\"{:?}\", self.services),\n    )\n  )]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<ComposeUpResponse> {\n    let ComposeUp {\n      mut stack,\n      repo,\n      services,\n      git_token,\n      registry_token,\n      mut replacers,\n    } = self;\n\n    let mut res = ComposeUpResponse::default();\n\n    let mut interpolator =\n      Interpolator::new(None, &periphery_config().secrets);\n    // Only interpolate Stack. Repo interpolation will be handled\n    // by the CloneRepo / PullOrCloneRepo call.\n    interpolator\n      .interpolate_stack(&mut stack)?\n      .push_logs(&mut res.logs);\n    replacers.extend(interpolator.secret_replacers);\n\n    let (run_directory, env_file_path) = match write_stack(\n      &stack,\n      repo.as_ref(),\n      git_token,\n      replacers.clone(),\n      &mut res,\n    )\n    .await\n    {\n      Ok(res) => res,\n      Err(e) => {\n        res\n          .logs\n          .push(Log::error(\"Write Stack\", format_serror(&e.into())));\n        return Ok(res);\n      }\n    };\n\n    // Canonicalize the path to ensure it exists, and is the cleanest path to the run directory.\n    let run_directory = run_directory.canonicalize().context(\n      \"Failed to validate run directory on host after stack write (canonicalize error)\",\n    )?;\n\n    validate_files(&stack, &run_directory, &mut res).await;\n    if !all_logs_success(&res.logs) {\n      return Ok(res);\n    }\n\n    maybe_login_registry(&stack, registry_token, &mut res.logs).await;\n    if !all_logs_success(&res.logs) {\n      return Ok(res);\n    }\n\n    // Pre deploy\n    if !stack.config.pre_deploy.is_none() {\n      let pre_deploy_path =\n        run_directory.join(&stack.config.pre_deploy.path);\n      if let Some(log) = run_komodo_command_with_sanitization(\n        \"Pre Deploy\",\n        pre_deploy_path.as_path(),\n        &stack.config.pre_deploy.command,\n        true,\n        &replacers,\n      )\n      .await\n      {\n        res.logs.push(log);\n        if !all_logs_success(&res.logs) {\n          return Ok(res);\n        }\n      };\n    }\n\n    let docker_compose = docker_compose();\n\n    let service_args = if services.is_empty() {\n      String::new()\n    } else {\n      format!(\" {}\", services.join(\" \"))\n    };\n\n    let file_args = stack.compose_file_paths().join(\" -f \");\n\n    // This will be the last project name, which is the one that needs to be destroyed.\n    // Might be different from the current project name, if user renames stack / changes to custom project name.\n    let last_project_name = stack.project_name(false);\n    let project_name = stack.project_name(true);\n\n    let env_file_args = env_file_args(\n      env_file_path,\n      &stack.config.additional_env_files,\n    )?;\n\n    // Uses 'docker compose config' command to extract services (including image)\n    // after performing interpolation\n    {\n      let command = format!(\n        \"{docker_compose} -p {project_name} -f {file_args}{env_file_args} config\",\n      );\n      let Some(config_log) = run_komodo_command_with_sanitization(\n        \"Compose Config\",\n        run_directory.as_path(),\n        command,\n        false,\n        &replacers,\n      )\n      .await\n      else {\n        // Only reachable if command is empty,\n        // not the case since it is provided above.\n        unreachable!()\n      };\n      if !config_log.success {\n        res.logs.push(config_log);\n        return Ok(res);\n      }\n      let compose =\n        serde_yaml_ng::from_str::<ComposeFile>(&config_log.stdout)\n          .context(\"Failed to parse compose contents\")?;\n      // Record sanitized compose config output\n      res.compose_config = Some(config_log.stdout);\n      for (\n        service_name,\n        ComposeService {\n          container_name,\n          deploy,\n          image,\n        },\n      ) in compose.services\n      {\n        let image = image.unwrap_or_default();\n        match deploy {\n          Some(ComposeServiceDeploy {\n            replicas: Some(replicas),\n          }) if replicas > 1 => {\n            for i in 1..1 + replicas {\n              res.services.push(StackServiceNames {\n                container_name: format!(\n                  \"{project_name}-{service_name}-{i}\"\n                ),\n                service_name: format!(\"{service_name}-{i}\"),\n                image: image.clone(),\n              });\n            }\n          }\n          _ => {\n            res.services.push(StackServiceNames {\n              container_name: container_name.unwrap_or_else(|| {\n                format!(\"{project_name}-{service_name}\")\n              }),\n              service_name,\n              image,\n            });\n          }\n        }\n      }\n    }\n\n    if stack.config.run_build {\n      let build_extra_args =\n        parse_extra_args(&stack.config.build_extra_args);\n      let command = format!(\n        \"{docker_compose} -p {project_name} -f {file_args}{env_file_args} build{build_extra_args}{service_args}\",\n      );\n      let Some(log) = run_komodo_command_with_sanitization(\n        \"Compose Build\",\n        run_directory.as_path(),\n        command,\n        false,\n        &replacers,\n      )\n      .await\n      else {\n        unreachable!()\n      };\n      res.logs.push(log);\n      if !all_logs_success(&res.logs) {\n        return Ok(res);\n      }\n    }\n\n    // Pull images before deploying\n    if stack.config.auto_pull {\n      // Pull images before destroying to minimize downtime.\n      // If this fails, do not continue.\n      let command = format!(\n        \"{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull{service_args}\",\n      );\n      let log = run_komodo_command(\n        \"Compose Pull\",\n        run_directory.as_ref(),\n        command,\n      )\n      .await;\n      res.logs.push(log);\n      if !all_logs_success(&res.logs) {\n        return Ok(res);\n      }\n    }\n\n    if stack.config.destroy_before_deploy\n      // Also check if project name changed, which also requires taking down.\n      || last_project_name != project_name\n    {\n      // Take down the existing containers.\n      // This one tries to use the previously deployed service name, to ensure the right stack is taken down.\n      crate::compose::down(&last_project_name, &services, &mut res)\n        .await\n        .context(\"failed to destroy existing containers\")?;\n    }\n\n    // Run compose up\n    let extra_args = parse_extra_args(&stack.config.extra_args);\n    let command = format!(\n      \"{docker_compose} -p {project_name} -f {file_args}{env_file_args} up -d{extra_args}{service_args}\",\n    );\n\n    let Some(log) = run_komodo_command_with_sanitization(\n      \"Compose Up\",\n      run_directory.as_path(),\n      command,\n      false,\n      &replacers,\n    )\n    .await\n    else {\n      unreachable!()\n    };\n\n    res.deployed = log.success;\n    res.logs.push(log);\n\n    if res.deployed && !stack.config.post_deploy.is_none() {\n      let post_deploy_path =\n        run_directory.join(&stack.config.post_deploy.path);\n      if let Some(log) = run_komodo_command_with_sanitization(\n        \"Post Deploy\",\n        post_deploy_path.as_path(),\n        &stack.config.post_deploy.command,\n        true,\n        &replacers,\n      )\n      .await\n      {\n        res.logs.push(log);\n      };\n    }\n\n    Ok(res)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for ComposeExecution {\n  #[instrument(name = \"ComposeExecution\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let ComposeExecution { project, command } = self;\n    let docker_compose = docker_compose();\n    let log = run_komodo_command(\n      \"Compose Command\",\n      None,\n      format!(\"{docker_compose} -p {project} {command}\"),\n    )\n    .await;\n    Ok(log)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for ComposeRun {\n  #[instrument(name = \"ComposeRun\", level = \"debug\", skip_all, fields(stack = &self.stack.name, service = &self.service))]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let ComposeRun {\n      mut stack,\n      repo,\n      git_token,\n      registry_token,\n      mut replacers,\n      service,\n      command,\n      no_tty,\n      no_deps,\n      detach,\n      service_ports,\n      env,\n      workdir,\n      user,\n      entrypoint,\n      pull,\n    } = self;\n\n    let mut interpolator =\n      Interpolator::new(None, &periphery_config().secrets);\n    interpolator\n      .interpolate_stack(&mut stack)?\n      .push_logs(&mut Vec::new());\n    replacers.extend(interpolator.secret_replacers);\n\n    let mut res = ComposeRunResponse::default();\n    let (run_directory, env_file_path) = match write_stack(\n      &stack,\n      repo.as_ref(),\n      git_token,\n      replacers.clone(),\n      &mut res,\n    )\n    .await\n    {\n      Ok(res) => res,\n      Err(e) => {\n        return Ok(Log::error(\n          \"Write Stack\",\n          format_serror(&e.into()),\n        ));\n      }\n    };\n\n    let run_directory = run_directory.canonicalize().context(\n      \"Failed to validate run directory on host after stack write (canonicalize error)\",\n    )?;\n\n    maybe_login_registry(&stack, registry_token, &mut Vec::new())\n      .await;\n\n    let docker_compose = docker_compose();\n\n    let file_args = if stack.config.file_paths.is_empty() {\n      String::from(\"compose.yaml\")\n    } else {\n      stack.config.file_paths.join(\" -f \")\n    };\n\n    let env_file_args = env_file_args(\n      env_file_path,\n      &stack.config.additional_env_files,\n    )?;\n\n    let project_name = stack.project_name(true);\n\n    if pull.unwrap_or_default() {\n      let pull_log = run_komodo_command(\n        \"Compose Pull\",\n        run_directory.as_ref(),\n        format!(\n          \"{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull {service}\",\n        ),\n      )\n      .await;\n      if !pull_log.success {\n        return Ok(pull_log);\n      }\n    }\n\n    let mut run_flags = String::from(\" --rm\");\n    if detach.unwrap_or_default() {\n      run_flags.push_str(\" -d\");\n    }\n    if no_tty.unwrap_or_default() {\n      run_flags.push_str(\" --no-tty\");\n    }\n    if no_deps.unwrap_or_default() {\n      run_flags.push_str(\" --no-deps\");\n    }\n    if service_ports.unwrap_or_default() {\n      run_flags.push_str(\" --service-ports\");\n    }\n    if let Some(dir) = workdir.as_ref() {\n      run_flags.push_str(&format!(\" --workdir {dir}\"));\n    }\n    if let Some(user) = user.as_ref() {\n      run_flags.push_str(&format!(\" --user {user}\"));\n    }\n    if let Some(entrypoint) = entrypoint.as_ref() {\n      run_flags.push_str(&format!(\" --entrypoint {entrypoint}\"));\n    }\n    if let Some(env) = env {\n      for (k, v) in env {\n        run_flags.push_str(&format!(\" -e {}={} \", k, v));\n      }\n    }\n\n    let command_args = command\n      .as_ref()\n      .filter(|v| !v.is_empty())\n      .map(|argv| {\n        let joined = argv\n          .iter()\n          .map(|s| escape(Cow::Borrowed(s)).into_owned())\n          .collect::<Vec<_>>()\n          .join(\" \");\n        format!(\" {joined}\")\n      })\n      .unwrap_or_default();\n\n    let command = format!(\n      \"{docker_compose} -p {project_name} -f {file_args}{env_file_args} run{run_flags} {service}{command_args}\",\n    );\n\n    let Some(log) = run_komodo_command_with_sanitization(\n      \"Compose Run\",\n      run_directory.as_path(),\n      command,\n      false,\n      &replacers,\n    )\n    .await\n    else {\n      unreachable!()\n    };\n\n    Ok(log)\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/api/container.rs",
    "content": "use anyhow::Context;\nuse command::run_komodo_command;\nuse futures::future::join_all;\nuse komodo_client::entities::{\n  docker::{\n    container::{Container, ContainerListItem, ContainerStats},\n    stats::FullContainerStats,\n  },\n  update::Log,\n};\nuse periphery_client::api::container::*;\nuse resolver_api::Resolve;\n\nuse crate::{\n  docker::{\n    docker_client, stats::get_container_stats, stop_container_command,\n  },\n  helpers::log_grep,\n};\n\n// ======\n//  READ\n// ======\n\n//\n\nimpl Resolve<super::Args> for InspectContainer {\n  #[instrument(name = \"InspectContainer\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Container> {\n    Ok(docker_client().inspect_container(&self.name).await?)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for GetContainerLog {\n  #[instrument(name = \"GetContainerLog\", level = \"debug\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let GetContainerLog {\n      name,\n      tail,\n      timestamps,\n    } = self;\n    let timestamps = if timestamps {\n      \" --timestamps\"\n    } else {\n      Default::default()\n    };\n    let command =\n      format!(\"docker logs {name} --tail {tail}{timestamps}\");\n    Ok(run_komodo_command(\"Get container log\", None, command).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for GetContainerLogSearch {\n  #[instrument(name = \"GetContainerLogSearch\", level = \"debug\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let GetContainerLogSearch {\n      name,\n      terms,\n      combinator,\n      invert,\n      timestamps,\n    } = self;\n    let grep = log_grep(&terms, combinator, invert);\n    let timestamps = if timestamps {\n      \" --timestamps\"\n    } else {\n      Default::default()\n    };\n    let command = format!(\n      \"docker logs {name} --tail 5000{timestamps} 2>&1 | {grep}\"\n    );\n    Ok(\n      run_komodo_command(\"Get container log grep\", None, command)\n        .await,\n    )\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for GetContainerStats {\n  #[instrument(name = \"GetContainerStats\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<ContainerStats> {\n    let mut stats = get_container_stats(Some(self.name)).await?;\n    let stats =\n      stats.pop().context(\"No stats found for container\")?;\n    Ok(stats)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for GetFullContainerStats {\n  #[instrument(name = \"GetFullContainerStats\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<FullContainerStats> {\n    docker_client()\n      .full_container_stats(&self.name)\n      .await\n      .map_err(Into::into)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for GetContainerStatsList {\n  #[instrument(name = \"GetContainerStatsList\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<ContainerStats>> {\n    Ok(get_container_stats(None).await?)\n  }\n}\n\n// =========\n//  ACTIONS\n// =========\n\nimpl Resolve<super::Args> for StartContainer {\n  #[instrument(name = \"StartContainer\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    Ok(\n      run_komodo_command(\n        \"Docker Start\",\n        None,\n        format!(\"docker start {}\", self.name),\n      )\n      .await,\n    )\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for RestartContainer {\n  #[instrument(name = \"RestartContainer\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    Ok(\n      run_komodo_command(\n        \"Docker Restart\",\n        None,\n        format!(\"docker restart {}\", self.name),\n      )\n      .await,\n    )\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for PauseContainer {\n  #[instrument(name = \"PauseContainer\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    Ok(\n      run_komodo_command(\n        \"Docker Pause\",\n        None,\n        format!(\"docker pause {}\", self.name),\n      )\n      .await,\n    )\n  }\n}\n\nimpl Resolve<super::Args> for UnpauseContainer {\n  #[instrument(name = \"UnpauseContainer\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    Ok(\n      run_komodo_command(\n        \"Docker Unpause\",\n        None,\n        format!(\"docker unpause {}\", self.name),\n      )\n      .await,\n    )\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for StopContainer {\n  #[instrument(name = \"StopContainer\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let StopContainer { name, signal, time } = self;\n    let command = stop_container_command(&name, signal, time);\n    let log = run_komodo_command(\"Docker Stop\", None, command).await;\n    if log.stderr.contains(\"unknown flag: --signal\") {\n      let command = stop_container_command(&name, None, time);\n      let mut log =\n        run_komodo_command(\"Docker Stop\", None, command).await;\n      log.stderr = format!(\n        \"old docker version: unable to use --signal flag{}\",\n        if !log.stderr.is_empty() {\n          format!(\"\\n\\n{}\", log.stderr)\n        } else {\n          String::new()\n        }\n      );\n      Ok(log)\n    } else {\n      Ok(log)\n    }\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for RemoveContainer {\n  #[instrument(name = \"RemoveContainer\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let RemoveContainer { name, signal, time } = self;\n    let stop_command = stop_container_command(&name, signal, time);\n    let command =\n      format!(\"{stop_command} && docker container rm {name}\");\n    let log =\n      run_komodo_command(\"Docker Stop and Remove\", None, command)\n        .await;\n    if log.stderr.contains(\"unknown flag: --signal\") {\n      let stop_command = stop_container_command(&name, None, time);\n      let command =\n        format!(\"{stop_command} && docker container rm {name}\");\n      let mut log =\n        run_komodo_command(\"Docker Stop and Remove\", None, command)\n          .await;\n      log.stderr = format!(\n        \"Old docker version: unable to use --signal flag{}\",\n        if !log.stderr.is_empty() {\n          format!(\"\\n\\n{}\", log.stderr)\n        } else {\n          String::new()\n        }\n      );\n      Ok(log)\n    } else {\n      Ok(log)\n    }\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for RenameContainer {\n  #[instrument(name = \"RenameContainer\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let RenameContainer {\n      curr_name,\n      new_name,\n    } = self;\n    let command = format!(\"docker rename {curr_name} {new_name}\");\n    Ok(run_komodo_command(\"Docker Rename\", None, command).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for PruneContainers {\n  #[instrument(name = \"PruneContainers\", skip_all)]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let command = String::from(\"docker container prune -f\");\n    Ok(run_komodo_command(\"Prune Containers\", None, command).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for StartAllContainers {\n  #[instrument(name = \"StartAllContainers\", skip_all)]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<Log>> {\n    let containers = docker_client()\n      .list_containers()\n      .await\n      .context(\"failed to list all containers on host\")?;\n    let futures = containers.iter().filter_map(\n      |ContainerListItem { name, labels, .. }| {\n        if labels.contains_key(\"komodo.skip\") {\n          return None;\n        }\n        let command = format!(\"docker start {name}\");\n        Some(async move {\n          run_komodo_command(&command.clone(), None, command).await\n        })\n      },\n    );\n    Ok(join_all(futures).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for RestartAllContainers {\n  #[instrument(name = \"RestartAllContainers\", skip_all)]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<Log>> {\n    let containers = docker_client()\n      .list_containers()\n      .await\n      .context(\"failed to list all containers on host\")?;\n    let futures = containers.iter().filter_map(\n      |ContainerListItem { name, labels, .. }| {\n        if labels.contains_key(\"komodo.skip\") {\n          return None;\n        }\n        let command = format!(\"docker restart {name}\");\n        Some(async move {\n          run_komodo_command(&command.clone(), None, command).await\n        })\n      },\n    );\n    Ok(join_all(futures).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for PauseAllContainers {\n  #[instrument(name = \"PauseAllContainers\", skip_all)]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<Log>> {\n    let containers = docker_client()\n      .list_containers()\n      .await\n      .context(\"failed to list all containers on host\")?;\n    let futures = containers.iter().filter_map(\n      |ContainerListItem { name, labels, .. }| {\n        if labels.contains_key(\"komodo.skip\") {\n          return None;\n        }\n        let command = format!(\"docker pause {name}\");\n        Some(async move {\n          run_komodo_command(&command.clone(), None, command).await\n        })\n      },\n    );\n    Ok(join_all(futures).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for UnpauseAllContainers {\n  #[instrument(name = \"UnpauseAllContainers\", skip_all)]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<Log>> {\n    let containers = docker_client()\n      .list_containers()\n      .await\n      .context(\"failed to list all containers on host\")?;\n    let futures = containers.iter().filter_map(\n      |ContainerListItem { name, labels, .. }| {\n        if labels.contains_key(\"komodo.skip\") {\n          return None;\n        }\n        let command = format!(\"docker unpause {name}\");\n        Some(async move {\n          run_komodo_command(&command.clone(), None, command).await\n        })\n      },\n    );\n    Ok(join_all(futures).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for StopAllContainers {\n  #[instrument(name = \"StopAllContainers\", skip_all)]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<Log>> {\n    let containers = docker_client()\n      .list_containers()\n      .await\n      .context(\"failed to list all containers on host\")?;\n    let futures = containers.iter().filter_map(\n      |ContainerListItem { name, labels, .. }| {\n        if labels.contains_key(\"komodo.skip\") {\n          return None;\n        }\n        Some(async move {\n          run_komodo_command(\n            &format!(\"docker stop {name}\"),\n            None,\n            stop_container_command(name, None, None),\n          )\n          .await\n        })\n      },\n    );\n    Ok(join_all(futures).await)\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/api/deploy.rs",
    "content": "use anyhow::Context;\nuse command::run_komodo_command_with_sanitization;\nuse formatting::format_serror;\nuse interpolate::Interpolator;\nuse komodo_client::{\n  entities::{\n    EnvironmentVar,\n    deployment::{\n      Conversion, Deployment, DeploymentConfig, DeploymentImage,\n      RestartMode, conversions_from_str, extract_registry_domain,\n    },\n    environment_vars_from_str,\n    update::Log,\n  },\n  parsers::QUOTE_PATTERN,\n};\nuse periphery_client::api::container::{Deploy, RemoveContainer};\nuse resolver_api::Resolve;\n\nuse crate::{\n  config::periphery_config,\n  docker::{docker_login, pull_image},\n  helpers::{parse_extra_args, parse_labels},\n};\n\nimpl Resolve<super::Args> for Deploy {\n  #[instrument(\n    name = \"Deploy\",\n    skip_all,\n    fields(\n      stack = &self.deployment.name,\n      stop_signal = format!(\"{:?}\", self.stop_signal),\n      stop_time = self.stop_time,\n    )\n  )]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let Deploy {\n      mut deployment,\n      stop_signal,\n      stop_time,\n      registry_token,\n      mut replacers,\n    } = self;\n\n    let mut interpolator =\n      Interpolator::new(None, &periphery_config().secrets);\n    interpolator.interpolate_deployment(&mut deployment)?;\n    replacers.extend(interpolator.secret_replacers);\n\n    let image = if let DeploymentImage::Image { image } =\n      &deployment.config.image\n    {\n      if image.is_empty() {\n        return Ok(Log::error(\n          \"get image\",\n          String::from(\"deployment does not have image attached\"),\n        ));\n      }\n      image\n    } else {\n      return Ok(Log::error(\n        \"get image\",\n        String::from(\"deployment does not have image attached\"),\n      ));\n    };\n\n    if let Err(e) = docker_login(\n      &extract_registry_domain(image)?,\n      &deployment.config.image_registry_account,\n      registry_token.as_deref(),\n    )\n    .await\n    {\n      return Ok(Log::error(\n        \"docker login\",\n        format_serror(\n          &e.context(\"failed to login to docker registry\").into(),\n        ),\n      ));\n    }\n\n    let _ = pull_image(image).await;\n    debug!(\"image pulled\");\n\n    let _ = (RemoveContainer {\n      name: deployment.name.clone(),\n      signal: stop_signal,\n      time: stop_time,\n    })\n    .resolve(&super::Args)\n    .await;\n    debug!(\"container stopped and removed\");\n\n    let command = docker_run_command(&deployment, image)\n      .context(\"Unable to generate valid docker run command\")?;\n\n    let Some(log) = run_komodo_command_with_sanitization(\n      \"Docker Run\",\n      None,\n      command,\n      false,\n      &replacers,\n    )\n    .await\n    else {\n      // The none case is only for empty command,\n      // this won't be the case given it is populated above.\n      unreachable!()\n    };\n\n    Ok(log)\n  }\n}\n\nfn docker_run_command(\n  Deployment {\n    name,\n    config:\n      DeploymentConfig {\n        volumes,\n        ports,\n        network,\n        command,\n        restart,\n        environment,\n        labels,\n        extra_args,\n        ..\n      },\n    ..\n  }: &Deployment,\n  image: &str,\n) -> anyhow::Result<String> {\n  let ports = parse_conversions(\n    &conversions_from_str(ports).context(\"Invalid ports\")?,\n    \"-p\",\n  );\n  let volumes = parse_conversions(\n    &conversions_from_str(volumes).context(\"Invalid volumes\")?,\n    \"-v\",\n  );\n  let network = parse_network(network);\n  let restart = parse_restart(restart);\n  let environment = parse_environment(\n    &environment_vars_from_str(environment)\n      .context(\"Invalid environment\")?,\n  );\n  let labels = parse_labels(\n    &environment_vars_from_str(labels).context(\"Invalid labels\")?,\n  );\n  let command = parse_command(command);\n  let extra_args = parse_extra_args(extra_args);\n  let command = format!(\n    \"docker run -d --name {name}{ports}{volumes}{network}{restart}{environment}{labels}{extra_args} {image}{command}\"\n  );\n  Ok(command)\n}\n\nfn parse_conversions(\n  conversions: &[Conversion],\n  flag: &str,\n) -> String {\n  conversions\n    .iter()\n    .map(|p| format!(\" {flag} {}:{}\", p.local, p.container))\n    .collect::<Vec<_>>()\n    .join(\"\")\n}\n\nfn parse_environment(environment: &[EnvironmentVar]) -> String {\n  environment\n    .iter()\n    .map(|p| {\n      if p.value.starts_with(QUOTE_PATTERN)\n        && p.value.ends_with(QUOTE_PATTERN)\n      {\n        // If the value already wrapped in quotes, don't wrap it again\n        format!(\" --env {}={}\", p.variable, p.value)\n      } else {\n        format!(\" --env {}=\\\"{}\\\"\", p.variable, p.value)\n      }\n    })\n    .collect::<Vec<_>>()\n    .join(\"\")\n}\n\nfn parse_network(network: &str) -> String {\n  format!(\" --network {network}\")\n}\n\nfn parse_restart(restart: &RestartMode) -> String {\n  let restart = match restart {\n    RestartMode::OnFailure => \"on-failure:10\".to_string(),\n    _ => restart.to_string(),\n  };\n  format!(\" --restart {restart}\")\n}\n\nfn parse_command(command: &str) -> String {\n  if command.is_empty() {\n    String::new()\n  } else {\n    format!(\" {command}\")\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/api/git.rs",
    "content": "use anyhow::{Context, anyhow};\nuse axum::http::StatusCode;\nuse formatting::format_serror;\nuse komodo_client::entities::{\n  DefaultRepoFolder, LatestCommit, update::Log,\n};\nuse periphery_client::api::git::{\n  CloneRepo, DeleteRepo, GetLatestCommit,\n  PeripheryRepoExecutionResponse, PullOrCloneRepo, PullRepo,\n  RenameRepo,\n};\nuse resolver_api::Resolve;\nuse serror::AddStatusCodeError;\nuse std::path::PathBuf;\nuse tokio::fs;\n\nuse crate::{\n  config::periphery_config, git::handle_post_repo_execution,\n};\n\nimpl Resolve<super::Args> for GetLatestCommit {\n  #[instrument(name = \"GetLatestCommit\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Option<LatestCommit>> {\n    let repo_path = match self.path {\n      Some(p) => PathBuf::from(p),\n      None => periphery_config().repo_dir().join(self.name),\n    };\n    // Make sure its a repo, or return null to avoid log spam\n    if !repo_path.is_dir() || !repo_path.join(\".git\").is_dir() {\n      return Ok(None);\n    }\n    Ok(Some(git::get_commit_hash_info(&repo_path).await?))\n  }\n}\n\nimpl Resolve<super::Args> for CloneRepo {\n  #[instrument(\n    name = \"CloneRepo\",\n    skip_all,\n    fields(\n      args = format!(\"{:?}\", self.args),\n      skip_secret_interp = self.skip_secret_interp,\n    )\n  )]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<PeripheryRepoExecutionResponse> {\n    let CloneRepo {\n      args,\n      git_token,\n      environment,\n      env_file_path,\n      on_clone,\n      on_pull,\n      skip_secret_interp,\n      replacers,\n    } = self;\n\n    let token = crate::helpers::git_token(git_token, &args)?;\n    let root_repo_dir = default_folder(args.default_folder)?;\n\n    let res = git::clone(args, &root_repo_dir, token).await?;\n\n    handle_post_repo_execution(\n      res,\n      environment,\n      &env_file_path,\n      on_clone,\n      on_pull,\n      skip_secret_interp,\n      replacers,\n    )\n    .await\n    .map_err(Into::into)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for PullRepo {\n  #[instrument(\n    name = \"PullRepo\",\n    skip_all,\n    fields(\n      args = format!(\"{:?}\", self.args),\n      skip_secret_interp = self.skip_secret_interp,\n    )\n  )]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<PeripheryRepoExecutionResponse> {\n    let PullRepo {\n      args,\n      git_token,\n      environment,\n      env_file_path,\n      on_pull,\n      skip_secret_interp,\n      replacers,\n    } = self;\n\n    let token = crate::helpers::git_token(git_token, &args)?;\n    let parent_dir = default_folder(args.default_folder)?;\n\n    let res = git::pull(args, &parent_dir, token).await?;\n\n    handle_post_repo_execution(\n      res,\n      environment,\n      &env_file_path,\n      None,\n      on_pull,\n      skip_secret_interp,\n      replacers,\n    )\n    .await\n    .map_err(Into::into)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for PullOrCloneRepo {\n  #[instrument(\n    name = \"PullOrCloneRepo\",\n    skip_all,\n    fields(\n      args = format!(\"{:?}\", self.args),\n      skip_secret_interp = self.skip_secret_interp,\n    )\n  )]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<PeripheryRepoExecutionResponse> {\n    let PullOrCloneRepo {\n      args,\n      git_token,\n      environment,\n      env_file_path,\n      on_clone,\n      on_pull,\n      skip_secret_interp,\n      replacers,\n    } = self;\n\n    let token = crate::helpers::git_token(git_token, &args)?;\n    let parent_dir = default_folder(args.default_folder)?;\n\n    let (res, cloned) =\n      git::pull_or_clone(args, &parent_dir, token).await?;\n\n    handle_post_repo_execution(\n      res,\n      environment,\n      &env_file_path,\n      cloned.then_some(on_clone).flatten(),\n      on_pull,\n      skip_secret_interp,\n      replacers,\n    )\n    .await\n    .map_err(Into::into)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for RenameRepo {\n  #[instrument(name = \"RenameRepo\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let RenameRepo {\n      curr_name,\n      new_name,\n    } = self;\n    let repo_dir = periphery_config().repo_dir();\n    let renamed =\n      fs::rename(repo_dir.join(&curr_name), repo_dir.join(&new_name))\n        .await;\n    let msg = match renamed {\n      Ok(_) => String::from(\"Renamed Repo directory on Server\"),\n      Err(_) => format!(\"No Repo cloned at {curr_name} to rename\"),\n    };\n    Ok(Log::simple(\"Rename Repo on Server\", msg))\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for DeleteRepo {\n  #[instrument(name = \"DeleteRepo\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let DeleteRepo { name, is_build } = self;\n    // If using custom clone path, it will be passed by core instead of name.\n    // So the join will resolve to just the absolute path.\n    let root = if is_build {\n      periphery_config().build_dir()\n    } else {\n      periphery_config().repo_dir()\n    };\n    let full_path = root.join(&name);\n    let deleted =\n      fs::remove_dir_all(&full_path).await.with_context(|| {\n        format!(\"Failed to delete repo at {full_path:?}\")\n      });\n    let log = match deleted {\n      Ok(_) => {\n        Log::simple(\"Delete repo\", format!(\"Deleted Repo {name}\"))\n      }\n      Err(e) => Log::error(\"Delete repo\", format_serror(&e.into())),\n    };\n    Ok(log)\n  }\n}\n\n//\n\nfn default_folder(\n  default_folder: DefaultRepoFolder,\n) -> serror::Result<PathBuf> {\n  match default_folder {\n    DefaultRepoFolder::Stacks => Ok(periphery_config().stack_dir()),\n    DefaultRepoFolder::Builds => Ok(periphery_config().build_dir()),\n    DefaultRepoFolder::Repos => Ok(periphery_config().repo_dir()),\n    DefaultRepoFolder::NotApplicable => {\n      Err(\n        anyhow!(\"The clone args should not have a default_folder of NotApplicable using this method.\")\n          .status_code(StatusCode::BAD_REQUEST)\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/api/image.rs",
    "content": "use std::sync::OnceLock;\n\nuse cache::TimeoutCache;\nuse command::run_komodo_command;\nuse komodo_client::entities::{\n  deployment::extract_registry_domain,\n  docker::image::{Image, ImageHistoryResponseItem},\n  komodo_timestamp,\n  update::Log,\n};\nuse periphery_client::api::image::*;\nuse resolver_api::Resolve;\n\nuse crate::docker::{docker_client, docker_login};\n\n//\n\nimpl Resolve<super::Args> for InspectImage {\n  #[instrument(name = \"InspectImage\", level = \"debug\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Image> {\n    Ok(docker_client().inspect_image(&self.name).await?)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for ImageHistory {\n  #[instrument(name = \"ImageHistory\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<ImageHistoryResponseItem>> {\n    Ok(docker_client().image_history(&self.name).await?)\n  }\n}\n\n//\n\n/// Wait this long after a pull to allow another pull through\nconst PULL_TIMEOUT: i64 = 5_000;\n\nfn pull_cache() -> &'static TimeoutCache<String, Log> {\n  static PULL_CACHE: OnceLock<TimeoutCache<String, Log>> =\n    OnceLock::new();\n  PULL_CACHE.get_or_init(Default::default)\n}\n\nimpl Resolve<super::Args> for PullImage {\n  #[instrument(name = \"PullImage\", skip_all, fields(name = &self.name))]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let PullImage {\n      name,\n      account,\n      token,\n    } = self;\n    // Acquire the image lock\n    let lock = pull_cache().get_lock(name.clone()).await;\n\n    // Lock the image lock, prevents simultaneous pulls by\n    // ensuring simultaneous pulls will wait for first to finish\n    // and checking cached results.\n    let mut locked = lock.lock().await;\n\n    // Early return from cache if lasted pulled with PULL_TIMEOUT\n    if locked.last_ts + PULL_TIMEOUT > komodo_timestamp() {\n      return locked.clone_res().map_err(Into::into);\n    }\n\n    let res = async {\n      docker_login(\n        &extract_registry_domain(&name)?,\n        account.as_deref().unwrap_or_default(),\n        token.as_deref(),\n      )\n      .await?;\n      anyhow::Ok(\n        run_komodo_command(\n          \"Docker Pull\",\n          None,\n          format!(\"docker pull {name}\"),\n        )\n        .await,\n      )\n    }\n    .await;\n\n    // Set the cache with results. Any other calls waiting on the lock will\n    // then immediately also use this same result.\n    locked.set(&res, komodo_timestamp());\n\n    res.map_err(Into::into)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for DeleteImage {\n  #[instrument(name = \"DeleteImage\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let command = format!(\"docker image rm {}\", self.name);\n    Ok(run_komodo_command(\"Delete Image\", None, command).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for PruneImages {\n  #[instrument(name = \"PruneImages\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let command = String::from(\"docker image prune -a -f\");\n    Ok(run_komodo_command(\"Prune Images\", None, command).await)\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/api/mod.rs",
    "content": "use anyhow::Context;\nuse command::run_komodo_command;\nuse derive_variants::EnumVariants;\nuse futures::TryFutureExt;\nuse komodo_client::entities::{\n  SystemCommand,\n  config::{DockerRegistry, GitProvider},\n  update::Log,\n};\nuse periphery_client::api::{\n  build::*, compose::*, container::*, git::*, image::*, network::*,\n  stats::*, terminal::*, volume::*, *,\n};\nuse resolver_api::Resolve;\nuse response::Response;\nuse serde::{Deserialize, Serialize};\n\nuse crate::{config::periphery_config, docker::docker_client};\n\nmod build;\nmod compose;\nmod container;\nmod deploy;\nmod git;\nmod image;\nmod network;\nmod router;\nmod stats;\nmod terminal;\nmod volume;\n\npub use router::router;\n\npub struct Args;\n\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,\n)]\n#[args(Args)]\n#[response(Response)]\n#[error(serror::Error)]\n#[variant_derive(Debug)]\n#[serde(tag = \"type\", content = \"params\")]\n#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]\npub enum PeripheryRequest {\n  GetVersion(GetVersion),\n  GetHealth(GetHealth),\n\n  // Config (Read)\n  ListGitProviders(ListGitProviders),\n  ListDockerRegistries(ListDockerRegistries),\n  ListSecrets(ListSecrets),\n\n  // Stats / Info (Read)\n  GetSystemInformation(GetSystemInformation),\n  GetSystemStats(GetSystemStats),\n  GetSystemProcesses(GetSystemProcesses),\n  GetLatestCommit(GetLatestCommit),\n\n  // Generic shell execution\n  RunCommand(RunCommand),\n\n  // Repo (Write)\n  CloneRepo(CloneRepo),\n  PullRepo(PullRepo),\n  PullOrCloneRepo(PullOrCloneRepo),\n  RenameRepo(RenameRepo),\n  DeleteRepo(DeleteRepo),\n\n  // Build\n  GetDockerfileContentsOnHost(GetDockerfileContentsOnHost),\n  WriteDockerfileContentsToHost(WriteDockerfileContentsToHost),\n  Build(Build),\n  PruneBuilders(PruneBuilders),\n  PruneBuildx(PruneBuildx),\n\n  // Compose (Read)\n  GetComposeContentsOnHost(GetComposeContentsOnHost),\n  GetComposeLog(GetComposeLog),\n  GetComposeLogSearch(GetComposeLogSearch),\n\n  // Compose (Write)\n  WriteComposeContentsToHost(WriteComposeContentsToHost),\n  WriteCommitComposeContents(WriteCommitComposeContents),\n  ComposePull(ComposePull),\n  ComposeUp(ComposeUp),\n  ComposeExecution(ComposeExecution),\n  ComposeRun(ComposeRun),\n\n  // Container (Read)\n  InspectContainer(InspectContainer),\n  GetContainerLog(GetContainerLog),\n  GetContainerLogSearch(GetContainerLogSearch),\n  GetContainerStats(GetContainerStats),\n  GetContainerStatsList(GetContainerStatsList),\n  GetFullContainerStats(GetFullContainerStats),\n\n  // Container (Write)\n  Deploy(Deploy),\n  StartContainer(StartContainer),\n  RestartContainer(RestartContainer),\n  PauseContainer(PauseContainer),\n  UnpauseContainer(UnpauseContainer),\n  StopContainer(StopContainer),\n  StartAllContainers(StartAllContainers),\n  RestartAllContainers(RestartAllContainers),\n  PauseAllContainers(PauseAllContainers),\n  UnpauseAllContainers(UnpauseAllContainers),\n  StopAllContainers(StopAllContainers),\n  RemoveContainer(RemoveContainer),\n  RenameContainer(RenameContainer),\n  PruneContainers(PruneContainers),\n\n  // Networks (Read)\n  InspectNetwork(InspectNetwork),\n\n  // Networks (Write)\n  CreateNetwork(CreateNetwork),\n  DeleteNetwork(DeleteNetwork),\n  PruneNetworks(PruneNetworks),\n\n  // Image (Read)\n  InspectImage(InspectImage),\n  ImageHistory(ImageHistory),\n\n  // Image (Write)\n  PullImage(PullImage),\n  DeleteImage(DeleteImage),\n  PruneImages(PruneImages),\n\n  // Volume (Read)\n  InspectVolume(InspectVolume),\n\n  // Volume (Write)\n  DeleteVolume(DeleteVolume),\n  PruneVolumes(PruneVolumes),\n\n  // All in one (Read)\n  GetDockerLists(GetDockerLists),\n\n  // All in one (Write)\n  PruneSystem(PruneSystem),\n\n  // Terminal\n  ListTerminals(ListTerminals),\n  CreateTerminal(CreateTerminal),\n  DeleteTerminal(DeleteTerminal),\n  DeleteAllTerminals(DeleteAllTerminals),\n  CreateTerminalAuthToken(CreateTerminalAuthToken),\n}\n\n//\n\nimpl Resolve<Args> for GetHealth {\n  #[instrument(name = \"GetHealth\", level = \"debug\", skip_all)]\n  async fn resolve(\n    self,\n    _: &Args,\n  ) -> serror::Result<GetHealthResponse> {\n    Ok(GetHealthResponse {})\n  }\n}\n\n//\n\nimpl Resolve<Args> for GetVersion {\n  #[instrument(name = \"GetVersion\", level = \"debug\", skip(self))]\n  async fn resolve(\n    self,\n    _: &Args,\n  ) -> serror::Result<GetVersionResponse> {\n    Ok(GetVersionResponse {\n      version: env!(\"CARGO_PKG_VERSION\").to_string(),\n    })\n  }\n}\n\n//\n\nimpl Resolve<Args> for ListGitProviders {\n  #[instrument(name = \"ListGitProviders\", level = \"debug\", skip_all)]\n  async fn resolve(\n    self,\n    _: &Args,\n  ) -> serror::Result<Vec<GitProvider>> {\n    Ok(periphery_config().git_providers.0.clone())\n  }\n}\n\nimpl Resolve<Args> for ListDockerRegistries {\n  #[instrument(\n    name = \"ListDockerRegistries\",\n    level = \"debug\",\n    skip_all\n  )]\n  async fn resolve(\n    self,\n    _: &Args,\n  ) -> serror::Result<Vec<DockerRegistry>> {\n    Ok(periphery_config().docker_registries.0.clone())\n  }\n}\n\n//\n\nimpl Resolve<Args> for ListSecrets {\n  #[instrument(name = \"ListSecrets\", level = \"debug\", skip_all)]\n  async fn resolve(self, _: &Args) -> serror::Result<Vec<String>> {\n    Ok(\n      periphery_config()\n        .secrets\n        .keys()\n        .cloned()\n        .collect::<Vec<_>>(),\n    )\n  }\n}\n\nimpl Resolve<Args> for GetDockerLists {\n  #[instrument(name = \"GetDockerLists\", level = \"debug\", skip_all)]\n  async fn resolve(\n    self,\n    _: &Args,\n  ) -> serror::Result<GetDockerListsResponse> {\n    let docker = docker_client();\n    let containers =\n      docker.list_containers().await.map_err(Into::into);\n    // Should still try to retrieve other docker lists, but \"in_use\" will be false for images, networks, volumes\n    let _containers = match &containers {\n      Ok(containers) => containers.as_slice(),\n      Err(_) => &[],\n    };\n    let (networks, images, volumes, projects) = tokio::join!(\n      docker.list_networks(_containers).map_err(Into::into),\n      docker.list_images(_containers).map_err(Into::into),\n      docker.list_volumes(_containers).map_err(Into::into),\n      ListComposeProjects {}\n        .resolve(&Args)\n        .map_err(|e| e.error.into())\n    );\n    Ok(GetDockerListsResponse {\n      containers,\n      networks,\n      images,\n      volumes,\n      projects,\n    })\n  }\n}\n\nimpl Resolve<Args> for RunCommand {\n  #[instrument(name = \"RunCommand\")]\n  async fn resolve(self, _: &Args) -> serror::Result<Log> {\n    let RunCommand {\n      command: SystemCommand { path, command },\n    } = self;\n    let res = tokio::spawn(async move {\n      let command = if path.is_empty() {\n        command\n      } else {\n        format!(\"cd {path} && {command}\")\n      };\n      run_komodo_command(\"run command\", None, command).await\n    })\n    .await\n    .context(\"failure in spawned task\")?;\n    Ok(res)\n  }\n}\n\nimpl Resolve<Args> for PruneSystem {\n  #[instrument(name = \"PruneSystem\", skip_all)]\n  async fn resolve(self, _: &Args) -> serror::Result<Log> {\n    let command = String::from(\"docker system prune -a -f --volumes\");\n    Ok(run_komodo_command(\"Prune System\", None, command).await)\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/api/network.rs",
    "content": "use command::run_komodo_command;\nuse komodo_client::entities::{\n  docker::network::Network, update::Log,\n};\nuse periphery_client::api::network::*;\nuse resolver_api::Resolve;\n\nuse crate::docker::docker_client;\n\n//\n\nimpl Resolve<super::Args> for InspectNetwork {\n  #[instrument(name = \"InspectNetwork\", level = \"debug\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Network> {\n    Ok(docker_client().inspect_network(&self.name).await?)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for CreateNetwork {\n  #[instrument(name = \"CreateNetwork\", skip(self))]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let CreateNetwork { name, driver } = self;\n    let driver = match driver {\n      Some(driver) => format!(\" -d {driver}\"),\n      None => String::new(),\n    };\n    let command = format!(\"docker network create{driver} {name}\");\n    Ok(run_komodo_command(\"Create Network\", None, command).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for DeleteNetwork {\n  #[instrument(name = \"DeleteNetwork\", skip(self))]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let command = format!(\"docker network rm {}\", self.name);\n    Ok(run_komodo_command(\"Delete Network\", None, command).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for PruneNetworks {\n  #[instrument(name = \"PruneNetworks\", skip(self))]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let command = String::from(\"docker network prune -f\");\n    Ok(run_komodo_command(\"Prune Networks\", None, command).await)\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/api/router.rs",
    "content": "use anyhow::{Context, anyhow};\nuse axum::{\n  Router,\n  body::Body,\n  extract::ConnectInfo,\n  http::{Request, StatusCode},\n  middleware::{self, Next},\n  response::Response,\n  routing::{get, post},\n};\nuse derive_variants::ExtractVariant;\nuse resolver_api::Resolve;\nuse serror::{AddStatusCode, AddStatusCodeError, Json};\nuse std::net::{IpAddr, SocketAddr};\nuse uuid::Uuid;\n\nuse crate::config::periphery_config;\n\npub fn router() -> Router {\n  Router::new()\n    .merge(\n      Router::new()\n        .route(\"/\", post(handler))\n        .layer(middleware::from_fn(guard_request_by_passkey)),\n    )\n    .nest(\n      \"/terminal\",\n      Router::new()\n        .route(\"/\", get(super::terminal::connect_terminal))\n        .route(\n          \"/container\",\n          get(super::terminal::connect_container_exec),\n        )\n        .nest(\n          \"/execute\",\n          Router::new()\n            .route(\"/\", post(super::terminal::execute_terminal))\n            .route(\n              \"/container\",\n              post(super::terminal::execute_container_exec),\n            )\n            .layer(middleware::from_fn(guard_request_by_passkey)),\n        ),\n    )\n    .layer(middleware::from_fn(guard_request_by_ip))\n}\n\nasync fn handler(\n  Json(request): Json<crate::api::PeripheryRequest>,\n) -> serror::Result<axum::response::Response> {\n  let req_id = Uuid::new_v4();\n\n  let res = tokio::spawn(task(req_id, request))\n    .await\n    .context(\"task handler spawn error\");\n\n  if let Err(e) = &res {\n    warn!(\"request {req_id} spawn error: {e:#}\");\n  }\n\n  res?\n}\n\nasync fn task(\n  req_id: Uuid,\n  request: crate::api::PeripheryRequest,\n) -> serror::Result<axum::response::Response> {\n  let variant = request.extract_variant();\n\n  let res = request.resolve(&crate::api::Args).await.map(|res| res.0);\n\n  if let Err(e) = &res {\n    warn!(\n      \"request {req_id} | type: {variant:?} | error: {:#}\",\n      e.error\n    );\n  }\n\n  res\n}\n\nasync fn guard_request_by_passkey(\n  req: Request<Body>,\n  next: Next,\n) -> serror::Result<Response> {\n  if periphery_config().passkeys.is_empty() {\n    return Ok(next.run(req).await);\n  }\n  let Some(req_passkey) = req.headers().get(\"authorization\") else {\n    return Err(\n      anyhow!(\"request was not sent with passkey\")\n        .status_code(StatusCode::UNAUTHORIZED),\n    );\n  };\n  let req_passkey = req_passkey\n    .to_str()\n    .context(\"failed to convert passkey to str\")\n    .status_code(StatusCode::UNAUTHORIZED)?;\n  if periphery_config()\n    .passkeys\n    .iter()\n    .any(|passkey| passkey == req_passkey)\n  {\n    Ok(next.run(req).await)\n  } else {\n    Err(\n      anyhow!(\"request passkey invalid\")\n        .status_code(StatusCode::UNAUTHORIZED),\n    )\n  }\n}\n\nasync fn guard_request_by_ip(\n  req: Request<Body>,\n  next: Next,\n) -> serror::Result<Response> {\n  if periphery_config().allowed_ips.is_empty() {\n    return Ok(next.run(req).await);\n  }\n  let ConnectInfo(socket_addr) = req\n    .extensions()\n    .get::<ConnectInfo<SocketAddr>>()\n    .context(\"could not get ConnectionInfo of request\")\n    .status_code(StatusCode::UNAUTHORIZED)?;\n  let ip = socket_addr.ip();\n\n  let ip_match = periphery_config().allowed_ips.iter().any(|net| {\n    net.contains(ip)\n      || match ip {\n        IpAddr::V4(ipv4) => {\n          net.contains(IpAddr::V6(ipv4.to_ipv6_mapped()))\n        }\n        IpAddr::V6(_) => net.contains(ip.to_canonical()),\n      }\n  });\n\n  if ip_match {\n    Ok(next.run(req).await)\n  } else {\n    Err(\n      anyhow!(\"requesting ip {ip} not allowed\")\n        .status_code(StatusCode::UNAUTHORIZED),\n    )\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/api/stats.rs",
    "content": "use komodo_client::entities::stats::{\n  SystemInformation, SystemProcess, SystemStats,\n};\nuse periphery_client::api::stats::{\n  GetSystemInformation, GetSystemProcesses, GetSystemStats,\n};\nuse resolver_api::Resolve;\n\nuse crate::stats::stats_client;\n\nimpl Resolve<super::Args> for GetSystemInformation {\n  #[instrument(\n    name = \"GetSystemInformation\",\n    level = \"debug\",\n    skip_all\n  )]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<SystemInformation> {\n    Ok(stats_client().read().await.info.clone())\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for GetSystemStats {\n  #[instrument(name = \"GetSystemStats\", level = \"debug\", skip_all)]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<SystemStats> {\n    Ok(stats_client().read().await.stats.clone())\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for GetSystemProcesses {\n  #[instrument(name = \"GetSystemProcesses\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<SystemProcess>> {\n    Ok(stats_client().read().await.get_processes())\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/api/terminal.rs",
    "content": "use anyhow::{Context, anyhow};\nuse axum::{\n  extract::{\n    Query, WebSocketUpgrade,\n    ws::{Message, Utf8Bytes},\n  },\n  http::StatusCode,\n  response::Response,\n};\nuse bytes::Bytes;\nuse futures::{SinkExt, StreamExt, TryStreamExt};\nuse komodo_client::{\n  api::write::TerminalRecreateMode,\n  entities::{KOMODO_EXIT_CODE, NoData, server::TerminalInfo},\n};\nuse periphery_client::api::terminal::*;\nuse resolver_api::Resolve;\nuse serror::{AddStatusCodeError, Json};\nuse tokio_util::sync::CancellationToken;\n\nuse crate::{config::periphery_config, terminal::*};\n\nimpl Resolve<super::Args> for ListTerminals {\n  #[instrument(name = \"ListTerminals\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<Vec<TerminalInfo>> {\n    clean_up_terminals().await;\n    Ok(list_terminals().await)\n  }\n}\n\nimpl Resolve<super::Args> for CreateTerminal {\n  #[instrument(name = \"CreateTerminal\", level = \"debug\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<NoData> {\n    if periphery_config().disable_terminals {\n      return Err(\n        anyhow!(\"Terminals are disabled in the periphery config\")\n          .status_code(StatusCode::FORBIDDEN),\n      );\n    }\n    create_terminal(self.name, self.command, self.recreate)\n      .await\n      .map(|_| NoData {})\n      .map_err(Into::into)\n  }\n}\n\nimpl Resolve<super::Args> for DeleteTerminal {\n  #[instrument(name = \"DeleteTerminal\", level = \"debug\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<NoData> {\n    delete_terminal(&self.terminal).await;\n    Ok(NoData {})\n  }\n}\n\nimpl Resolve<super::Args> for DeleteAllTerminals {\n  #[instrument(name = \"DeleteAllTerminals\", level = \"debug\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<NoData> {\n    delete_all_terminals().await;\n    Ok(NoData {})\n  }\n}\n\nimpl Resolve<super::Args> for CreateTerminalAuthToken {\n  #[instrument(name = \"CreateTerminalAuthToken\", level = \"debug\")]\n  async fn resolve(\n    self,\n    _: &super::Args,\n  ) -> serror::Result<CreateTerminalAuthTokenResponse> {\n    Ok(CreateTerminalAuthTokenResponse {\n      token: auth_tokens().create_auth_token(),\n    })\n  }\n}\n\npub async fn connect_terminal(\n  Query(query): Query<ConnectTerminalQuery>,\n  ws: WebSocketUpgrade,\n) -> serror::Result<Response> {\n  if periphery_config().disable_terminals {\n    return Err(\n      anyhow!(\"Terminals are disabled in the periphery config\")\n        .status_code(StatusCode::FORBIDDEN),\n    );\n  }\n  handle_terminal_websocket(query, ws).await\n}\n\npub async fn connect_container_exec(\n  Query(ConnectContainerExecQuery {\n    token,\n    container,\n    shell,\n  }): Query<ConnectContainerExecQuery>,\n  ws: WebSocketUpgrade,\n) -> serror::Result<Response> {\n  if periphery_config().disable_container_exec {\n    return Err(\n      anyhow!(\"Container exec is disabled in the periphery config\")\n        .into(),\n    );\n  }\n  if container.contains(\"&&\") || shell.contains(\"&&\") {\n    return Err(\n      anyhow!(\n        \"The use of '&&' is forbidden in the container name or shell\"\n      )\n      .into(),\n    );\n  }\n  // Create (recreate if shell changed)\n  create_terminal(\n    container.clone(),\n    format!(\"docker exec -it {container} {shell}\"),\n    TerminalRecreateMode::DifferentCommand,\n  )\n  .await\n  .context(\"Failed to create terminal for container exec\")?;\n\n  handle_terminal_websocket(\n    ConnectTerminalQuery {\n      token,\n      terminal: container,\n    },\n    ws,\n  )\n  .await\n}\n\nasync fn handle_terminal_websocket(\n  ConnectTerminalQuery { token, terminal }: ConnectTerminalQuery,\n  ws: WebSocketUpgrade,\n) -> serror::Result<Response> {\n  // Auth the connection with single use token\n  auth_tokens().check_token(token)?;\n\n  clean_up_terminals().await;\n  let terminal = get_terminal(&terminal).await?;\n\n  Ok(ws.on_upgrade(|mut socket| async move {\n    let init_res = async {\n      let (a, b) = terminal.history.bytes_parts();\n      if !a.is_empty() {\n        socket.send(Message::Binary(a)).await.context(\"Failed to send history part a\")?;\n      }\n      if !b.is_empty() {\n        socket.send(Message::Binary(b)).await.context(\"Failed to send history part b\")?;\n      }\n      anyhow::Ok(())\n    }.await;\n\n    if let Err(e) = init_res {\n      let _ = socket.send(Message::Text(format!(\"ERROR: {e:#}\").into())).await;\n      let _ = socket.close().await;\n      return;\n    }\n\n    let (mut ws_write, mut ws_read) = socket.split();\n\n    let cancel = CancellationToken::new();\n\n    let ws_read = async {\n      loop {\n        let res = tokio::select! {\n          res = ws_read.next() => res,\n          _ = terminal.cancel.cancelled() => {\n            trace!(\"ws read: cancelled from outside\");\n            break\n          },\n          _ = cancel.cancelled() => {\n            trace!(\"ws read: cancelled from inside\");\n            break;\n          }\n        };\n        match res {\n          Some(Ok(Message::Binary(bytes)))\n            if bytes.first() == Some(&0x00) =>\n          {\n            // println!(\"Got ws read bytes - for stdin\");\n            if let Err(e) = terminal.stdin.send(StdinMsg::Bytes(\n              Bytes::copy_from_slice(&bytes[1..]),\n            )).await {\n              debug!(\"WS -> PTY channel send error: {e:}\");\n              terminal.cancel();\n              break;\n            };\n          }\n          Some(Ok(Message::Binary(bytes)))\n            if bytes.first() == Some(&0xFF) =>\n          {\n            // println!(\"Got ws read bytes - for resize\");\n            if let Ok(dimensions) =\n              serde_json::from_slice::<ResizeDimensions>(&bytes[1..])\n                && let Err(e) = terminal.stdin.send(StdinMsg::Resize(dimensions)).await\n            {\n              debug!(\"WS -> PTY channel send error: {e:}\");\n              terminal.cancel();\n              break;\n            }\n          }\n          Some(Ok(Message::Text(text))) => {\n            trace!(\"Got ws read text\");\n            if let Err(e) =\n              terminal.stdin.send(StdinMsg::Bytes(Bytes::from(text))).await\n            {\n              debug!(\"WS -> PTY channel send error: {e:?}\");\n              terminal.cancel();\n              break;\n            };\n          }\n          Some(Ok(Message::Close(_))) => {\n            debug!(\"got ws read close\");\n            cancel.cancel();\n            break;\n          }\n          Some(Ok(_)) => {\n            // Do nothing (ping, non-prefixed bytes, etc.)\n          }\n          Some(Err(e)) => {\n            debug!(\"Got ws read error: {e:?}\");\n            cancel.cancel();\n            break;\n          }\n          None => {\n            debug!(\"Got ws read none\");\n            cancel.cancel();\n            break;\n          }\n        }\n      }\n    };\n\n    let ws_write = async {\n      let mut stdout = terminal.stdout.resubscribe();\n      loop {\n        let res = tokio::select! {\n          res = stdout.recv() => res.context(\"Failed to get message over stdout receiver\"),\n          _ = terminal.cancel.cancelled() => {\n            trace!(\"ws write: cancelled from outside\");\n            let _ = ws_write.send(Message::Text(Utf8Bytes::from_static(\"PTY KILLED\"))).await;\n            if let Err(e) = ws_write.close().await {\n              debug!(\"Failed to close ws: {e:?}\");\n            };\n            break\n          },\n          _ = cancel.cancelled() => {\n            let _ = ws_write.send(Message::Text(Utf8Bytes::from_static(\"WS KILLED\"))).await;\n            if let Err(e) = ws_write.close().await {\n              debug!(\"Failed to close ws: {e:?}\");\n            };\n            break\n          }\n        };\n        match res {\n          Ok(bytes) => {\n            if let Err(e) =\n              ws_write.send(Message::Binary(bytes)).await\n            {\n              debug!(\"Failed to send to WS: {e:?}\");\n              cancel.cancel();\n              break;\n            }\n          }\n          Err(e) => {\n            debug!(\"PTY -> WS channel read error: {e:?}\");\n            let _ = ws_write.send(Message::Text(Utf8Bytes::from(format!(\"ERROR: {e:#}\")))).await;\n            let _ = ws_write.close().await;\n            terminal.cancel();\n            break;\n          }\n        }\n      }\n    };\n\n    tokio::join!(ws_read, ws_write);\n\n    clean_up_terminals().await;\n  }))\n}\n\npub async fn execute_terminal(\n  Json(ExecuteTerminalBody { terminal, command }): Json<\n    ExecuteTerminalBody,\n  >,\n) -> serror::Result<axum::body::Body> {\n  if periphery_config().disable_terminals {\n    return Err(\n      anyhow!(\"Terminals are disabled in the periphery config\")\n        .status_code(StatusCode::FORBIDDEN),\n    );\n  }\n\n  execute_command_on_terminal(&terminal, &command).await\n}\n\npub async fn execute_container_exec(\n  Json(ExecuteContainerExecBody {\n    container,\n    shell,\n    command,\n  }): Json<ExecuteContainerExecBody>,\n) -> serror::Result<axum::body::Body> {\n  if periphery_config().disable_container_exec {\n    return Err(\n      anyhow!(\"Container exec is disabled in the periphery config\")\n        .into(),\n    );\n  }\n  if container.contains(\"&&\") || shell.contains(\"&&\") {\n    return Err(\n      anyhow!(\n        \"The use of '&&' is forbidden in the container name or shell\"\n      )\n      .into(),\n    );\n  }\n  // Create terminal (recreate if shell changed)\n  create_terminal(\n    container.clone(),\n    format!(\"docker exec -it {container} {shell}\"),\n    TerminalRecreateMode::DifferentCommand,\n  )\n  .await\n  .context(\"Failed to create terminal for container exec\")?;\n\n  execute_command_on_terminal(&container, &command).await\n}\n\nasync fn execute_command_on_terminal(\n  terminal_name: &str,\n  command: &str,\n) -> serror::Result<axum::body::Body> {\n  let terminal = get_terminal(terminal_name).await?;\n\n  // Read the bytes into lines\n  // This is done to check the lines for the EOF sentinal\n  let mut stdout = tokio_util::codec::FramedRead::new(\n    tokio_util::io::StreamReader::new(\n      tokio_stream::wrappers::BroadcastStream::new(\n        terminal.stdout.resubscribe(),\n      )\n      .map(|res| res.map_err(std::io::Error::other)),\n    ),\n    tokio_util::codec::LinesCodec::new(),\n  );\n\n  let full_command = format!(\n    \"printf '\\n{START_OF_OUTPUT}\\n\\n'; {command}; rc=$?; printf '\\n{KOMODO_EXIT_CODE}%d\\n{END_OF_OUTPUT}\\n' \\\"$rc\\\"\\n\"\n  );\n\n  terminal\n    .stdin\n    .send(StdinMsg::Bytes(Bytes::from(full_command)))\n    .await\n    .context(\"Failed to send command to terminal stdin\")?;\n\n  // Only start the response AFTER the start sentinel is printed\n  loop {\n    match stdout\n      .try_next()\n      .await\n      .context(\"Failed to read stdout line\")?\n    {\n      Some(line) if line == START_OF_OUTPUT => break,\n      // Keep looping until the start sentinel received.\n      Some(_) => {}\n      None => {\n        return Err(\n          anyhow!(\n            \"Stdout stream terminated before start sentinel received\"\n          )\n          .into(),\n        );\n      }\n    }\n  }\n\n  Ok(axum::body::Body::from_stream(TerminalStream { stdout }))\n}\n"
  },
  {
    "path": "bin/periphery/src/api/volume.rs",
    "content": "use command::run_komodo_command;\nuse komodo_client::entities::{docker::volume::Volume, update::Log};\nuse periphery_client::api::volume::*;\nuse resolver_api::Resolve;\n\nuse crate::docker::docker_client;\n\n//\n\nimpl Resolve<super::Args> for InspectVolume {\n  #[instrument(name = \"InspectVolume\", level = \"debug\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Volume> {\n    Ok(docker_client().inspect_volume(&self.name).await?)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for DeleteVolume {\n  #[instrument(name = \"DeleteVolume\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let command = format!(\"docker volume rm {}\", self.name);\n    Ok(run_komodo_command(\"Delete Volume\", None, command).await)\n  }\n}\n\n//\n\nimpl Resolve<super::Args> for PruneVolumes {\n  #[instrument(name = \"PruneVolumes\")]\n  async fn resolve(self, _: &super::Args) -> serror::Result<Log> {\n    let command = String::from(\"docker volume prune -a -f\");\n    Ok(run_komodo_command(\"Prune Volumes\", None, command).await)\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/build.rs",
    "content": "use std::{\n  fmt::Write,\n  path::{Path, PathBuf},\n};\n\nuse anyhow::{Context, anyhow};\nuse formatting::format_serror;\nuse komodo_client::{\n  entities::{EnvironmentVar, update::Log},\n  parsers::QUOTE_PATTERN,\n};\n\npub async fn write_dockerfile(\n  build_path: &Path,\n  dockerfile_path: &str,\n  dockerfile: &str,\n  logs: &mut Vec<Log>,\n) {\n  if let Err(e) = async {\n    if dockerfile.is_empty() {\n      return Err(anyhow!(\"UI Defined dockerfile is empty\"));\n    }\n\n    let full_dockerfile_path = build_path\n      .join(dockerfile_path)\n      .components()\n      .collect::<PathBuf>();\n\n    // Ensure parent directory exists\n    if let Some(parent) = full_dockerfile_path.parent() && !parent.exists() {\n      tokio::fs::create_dir_all(parent)\n        .await\n        .with_context(|| format!(\"Failed to initialize dockerfile parent directory {parent:?}\"))?;\n    }\n\n    tokio::fs::write(&full_dockerfile_path, dockerfile).await.with_context(|| {\n      format!(\n        \"Failed to write dockerfile contents to {full_dockerfile_path:?}\"\n      )\n    })?;\n\n    logs.push(Log::simple(\n      \"Write Dockerfile\",\n      format!(\n        \"Dockerfile contents written to {full_dockerfile_path:?}\"\n      ),\n    ));\n\n    anyhow::Ok(())\n  }.await {\n    logs.push(Log::error(\"Write Dockerfile\", format_serror(&e.into())));\n  }\n}\n\npub fn parse_build_args(build_args: &[EnvironmentVar]) -> String {\n  build_args\n    .iter()\n    .map(|p| {\n      if p.value.starts_with(QUOTE_PATTERN)\n        && p.value.ends_with(QUOTE_PATTERN)\n      {\n        // If the value already wrapped in quotes, don't wrap it again\n        format!(\" --build-arg {}={}\", p.variable, p.value)\n      } else {\n        format!(\" --build-arg {}=\\\"{}\\\"\", p.variable, p.value)\n      }\n    })\n    .collect::<Vec<_>>()\n    .join(\"\")\n}\n\n/// <https://docs.docker.com/build/building/secrets/#using-build-secrets>\npub async fn parse_secret_args(\n  secret_args: &[EnvironmentVar],\n  build_dir: &Path,\n) -> anyhow::Result<String> {\n  let mut res = String::new();\n  for EnvironmentVar { variable, value } in secret_args {\n    // Check edge cases\n    if variable.is_empty() {\n      return Err(anyhow!(\"secret variable cannot be empty string\"));\n    } else if variable.contains('=') {\n      return Err(anyhow!(\n        \"invalid variable {variable}. variable cannot contain '='\"\n      ));\n    }\n    // Write the value to file to mount\n    let path = build_dir.join(variable);\n    tokio::fs::write(&path, value).await.with_context(|| {\n      format!(\n        \"Failed to write build secret {variable} to {}\",\n        path.display()\n      )\n    })?;\n    // Extend the command\n    write!(\n      &mut res,\n      \" --secret id={variable},src={}\",\n      path.display()\n    )\n    .with_context(|| {\n      format!(\n        \"Failed to format build secret arguments for {variable}\"\n      )\n    })?;\n  }\n  Ok(res)\n}\n"
  },
  {
    "path": "bin/periphery/src/compose/mod.rs",
    "content": "use std::{fmt::Write, path::PathBuf};\n\nuse anyhow::{Context, anyhow};\nuse command::run_komodo_command;\nuse komodo_client::entities::{\n  RepoExecutionArgs, repo::Repo, stack::Stack,\n  to_path_compatible_name,\n};\nuse periphery_client::api::{\n  compose::ComposeUpResponse, git::PullOrCloneRepo,\n};\nuse resolver_api::Resolve;\n\nuse crate::config::periphery_config;\n\npub mod up;\npub mod write;\n\npub fn docker_compose() -> &'static str {\n  if periphery_config().legacy_compose_cli {\n    \"docker-compose\"\n  } else {\n    \"docker compose\"\n  }\n}\n\npub fn env_file_args(\n  env_file_path: Option<&str>,\n  additional_env_files: &[String],\n) -> anyhow::Result<String> {\n  let mut res = String::new();\n\n  for file in additional_env_files.iter().filter(|&path| {\n    let Some(komodo_path) = env_file_path else {\n      return true;\n    };\n    // Filter komodo env out of additional env file if its also in there.\n    // It will be always be added last / have highest priority.\n    path != komodo_path\n  }) {\n    write!(res, \" --env-file {file}\").with_context(|| {\n      format!(\"Failed to write --env-file arg for {file}\")\n    })?;\n  }\n\n  // Add this last, so it is applied on top\n  if let Some(file) = env_file_path {\n    write!(res, \" --env-file {file}\").with_context(|| {\n      format!(\"Failed to write --env-file arg for {file}\")\n    })?;\n  }\n\n  Ok(res)\n}\n\npub async fn down(\n  project: &str,\n  services: &[String],\n  res: &mut ComposeUpResponse,\n) -> anyhow::Result<()> {\n  let docker_compose = docker_compose();\n  let service_args = if services.is_empty() {\n    String::new()\n  } else {\n    format!(\" {}\", services.join(\" \"))\n  };\n  let log = run_komodo_command(\n    \"Compose Down\",\n    None,\n    format!(\"{docker_compose} -p {project} down{service_args}\"),\n  )\n  .await;\n  let success = log.success;\n  res.logs.push(log);\n  if !success {\n    return Err(anyhow!(\n      \"Failed to bring down existing container(s) with docker compose down. Stopping run.\"\n    ));\n  }\n\n  Ok(())\n}\n\n/// Only for git repo based Stacks.\n/// Returns path to root directory of the stack repo.\n///\n/// Both Stack and Repo environment, on clone, on pull are ignored.\npub async fn pull_or_clone_stack(\n  stack: &Stack,\n  repo: Option<&Repo>,\n  git_token: Option<String>,\n) -> anyhow::Result<PathBuf> {\n  if stack.config.files_on_host {\n    return Err(anyhow!(\n      \"Wrong method called for files on host stack\"\n    ));\n  }\n  if repo.is_none() && stack.config.repo.is_empty() {\n    return Err(anyhow!(\"Repo is not configured\"));\n  }\n\n  let (root, mut args) = if let Some(repo) = repo {\n    let root = periphery_config()\n      .repo_dir()\n      .join(to_path_compatible_name(&repo.name))\n      .join(&repo.config.path)\n      .components()\n      .collect::<PathBuf>();\n    let args: RepoExecutionArgs = repo.into();\n    (root, args)\n  } else {\n    let root = periphery_config()\n      .stack_dir()\n      .join(to_path_compatible_name(&stack.name))\n      .join(&stack.config.clone_path)\n      .components()\n      .collect::<PathBuf>();\n    let args: RepoExecutionArgs = stack.into();\n    (root, args)\n  };\n  args.destination = Some(root.display().to_string());\n\n  let git_token = crate::helpers::git_token(git_token, &args)?;\n\n  PullOrCloneRepo {\n    args,\n    git_token,\n    // All the extra pull functions\n    // (env, on clone, on pull)\n    // are disabled with this method.\n    environment: Default::default(),\n    env_file_path: Default::default(),\n    on_clone: Default::default(),\n    on_pull: Default::default(),\n    skip_secret_interp: Default::default(),\n    replacers: Default::default(),\n  }\n  .resolve(&crate::api::Args)\n  .await\n  .map_err(|e| e.error)?;\n\n  Ok(root)\n}\n"
  },
  {
    "path": "bin/periphery/src/compose/up.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse anyhow::{Context, anyhow};\nuse formatting::format_serror;\nuse komodo_client::entities::{\n  FileContents,\n  stack::{Stack, StackRemoteFileContents},\n  update::Log,\n};\nuse periphery_client::api::compose::ComposeUpResponse;\nuse tokio::fs;\n\nuse crate::docker::docker_login;\n\npub async fn validate_files(\n  stack: &Stack,\n  run_directory: &Path,\n  res: &mut ComposeUpResponse,\n) {\n  let file_paths = stack\n    .all_file_dependencies()\n    .into_iter()\n    .map(|file| {\n      (\n        // This will remove any intermediate uneeded '/./' in the path\n        run_directory\n          .join(&file.path)\n          .components()\n          .collect::<PathBuf>(),\n        file,\n      )\n    })\n    .collect::<Vec<_>>();\n\n  // First validate no missing files\n  for (full_path, file) in &file_paths {\n    if !full_path.exists() {\n      res.missing_files.push(file.path.clone());\n    }\n  }\n  if !res.missing_files.is_empty() {\n    res.logs.push(Log::error(\n      \"Validate Files\",\n      format_serror(\n        &anyhow!(\n          \"Missing files: {}\", res.missing_files.join(\", \")\n        )\n        .context(\"Ensure the run_directory and all file paths are correct.\")\n        .context(\"A file doesn't exist after writing stack.\")\n        .into(),\n      ),\n    ));\n    return;\n  }\n\n  for (full_path, file) in file_paths {\n    let file_contents =\n      match fs::read_to_string(&full_path).await.with_context(|| {\n        format!(\"Failed to read file contents at {full_path:?}\")\n      }) {\n        Ok(res) => res,\n        Err(e) => {\n          let error = format_serror(&e.into());\n          res\n            .logs\n            .push(Log::error(\"Read Compose File\", error.clone()));\n          // This should only happen for repo stacks, ie remote error\n          res.remote_errors.push(FileContents {\n            path: file.path,\n            contents: error,\n          });\n          return;\n        }\n      };\n    res.file_contents.push(StackRemoteFileContents {\n      path: file.path,\n      contents: file_contents,\n      services: file.services,\n      requires: file.requires,\n    });\n  }\n}\n\npub async fn maybe_login_registry(\n  stack: &Stack,\n  registry_token: Option<String>,\n  logs: &mut Vec<Log>,\n) {\n  if !stack.config.registry_provider.is_empty()\n    && !stack.config.registry_account.is_empty()\n    && let Err(e) = docker_login(\n      &stack.config.registry_provider,\n      &stack.config.registry_account,\n      registry_token.as_deref(),\n    )\n    .await\n    .with_context(|| {\n      format!(\n        \"Domain: '{}' | Account: '{}'\",\n        stack.config.registry_provider, stack.config.registry_account\n      )\n    })\n    .context(\"Failed to login to image registry\")\n  {\n    logs.push(Log::error(\n      \"Login to Registry\",\n      format_serror(&e.into()),\n    ));\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/compose/write.rs",
    "content": "use std::path::PathBuf;\n\nuse anyhow::{Context, anyhow};\nuse formatting::format_serror;\nuse komodo_client::entities::{\n  FileContents, RepoExecutionArgs, all_logs_success, repo::Repo,\n  stack::Stack, to_path_compatible_name, update::Log,\n};\nuse periphery_client::api::{\n  compose::{\n    ComposePullResponse, ComposeRunResponse, ComposeUpResponse,\n  },\n  git::{CloneRepo, PullOrCloneRepo},\n};\nuse resolver_api::Resolve;\nuse tokio::fs;\n\nuse crate::{config::periphery_config, helpers};\n\npub trait WriteStackRes {\n  fn logs(&mut self) -> &mut Vec<Log>;\n  fn add_remote_error(&mut self, _contents: FileContents) {}\n  fn set_commit_hash(&mut self, _hash: Option<String>) {}\n  fn set_commit_message(&mut self, _message: Option<String>) {}\n}\n\nimpl WriteStackRes for &mut ComposeUpResponse {\n  fn logs(&mut self) -> &mut Vec<Log> {\n    &mut self.logs\n  }\n  fn add_remote_error(&mut self, contents: FileContents) {\n    self.remote_errors.push(contents);\n  }\n  fn set_commit_hash(&mut self, hash: Option<String>) {\n    self.commit_hash = hash;\n  }\n  fn set_commit_message(&mut self, message: Option<String>) {\n    self.commit_message = message;\n  }\n}\n\nimpl WriteStackRes for &mut ComposePullResponse {\n  fn logs(&mut self) -> &mut Vec<Log> {\n    &mut self.logs\n  }\n}\n\nimpl WriteStackRes for &mut ComposeRunResponse {\n  fn logs(&mut self) -> &mut Vec<Log> {\n    &mut self.logs\n  }\n}\n\n/// Either writes the stack file_contents to a file, or clones the repo.\n/// Asssumes all interpolation is already complete.\n/// Returns (run_directory, env_file_path, periphery_replacers)\npub async fn write_stack<'a>(\n  stack: &'a Stack,\n  repo: Option<&Repo>,\n  git_token: Option<String>,\n  replacers: Vec<(String, String)>,\n  res: impl WriteStackRes,\n) -> anyhow::Result<(\n  // run_directory\n  PathBuf,\n  // env_file_path\n  Option<&'a str>,\n)> {\n  if stack.config.files_on_host {\n    write_stack_files_on_host(stack, res).await\n  } else if let Some(repo) = repo {\n    write_stack_linked_repo(stack, repo, git_token, replacers, res)\n      .await\n  } else if !stack.config.repo.is_empty() {\n    write_stack_inline_repo(stack, git_token, res).await\n  } else {\n    write_stack_ui_defined(stack, res).await\n  }\n}\n\nasync fn write_stack_files_on_host(\n  stack: &Stack,\n  mut res: impl WriteStackRes,\n) -> anyhow::Result<(\n  // run_directory\n  PathBuf,\n  // env_file_path\n  Option<&str>,\n)> {\n  let run_directory = periphery_config()\n    .stack_dir()\n    .join(to_path_compatible_name(&stack.name))\n    .join(&stack.config.run_directory)\n    .components()\n    .collect::<PathBuf>();\n  let env_file_path = environment::write_env_file(\n    &stack.config.env_vars()?,\n    run_directory.as_path(),\n    &stack.config.env_file_path,\n    res.logs(),\n  )\n  .await;\n  if all_logs_success(res.logs()) {\n    Ok((\n      run_directory,\n      // Env file paths are expected to be already relative to run directory,\n      // so need to pass original env_file_path here.\n      env_file_path\n        .is_some()\n        .then_some(&stack.config.env_file_path),\n    ))\n  } else {\n    Err(anyhow!(\"Failed to write env file, stopping run.\"))\n  }\n}\n\nasync fn write_stack_linked_repo<'a>(\n  stack: &'a Stack,\n  repo: &Repo,\n  git_token: Option<String>,\n  replacers: Vec<(String, String)>,\n  mut res: impl WriteStackRes,\n) -> anyhow::Result<(\n  // run_directory\n  PathBuf,\n  // env_file_path\n  Option<&'a str>,\n)> {\n  let root = periphery_config()\n    .repo_dir()\n    .join(to_path_compatible_name(&repo.name))\n    .join(&repo.config.path)\n    .components()\n    .collect::<PathBuf>();\n\n  let mut args: RepoExecutionArgs = repo.into();\n  // Set the clone destination to the one created for this run\n  args.destination = Some(root.display().to_string());\n\n  let git_token = stack_git_token(git_token, &args, &mut res)?;\n\n  let env_file_path = root\n    .join(&repo.config.env_file_path)\n    .components()\n    .collect::<PathBuf>()\n    .display()\n    .to_string();\n\n  let on_clone = (!repo.config.on_clone.is_none())\n    .then_some(repo.config.on_clone.clone());\n  let on_pull = (!repo.config.on_pull.is_none())\n    .then_some(repo.config.on_pull.clone());\n\n  let clone_res = if stack.config.reclone {\n    CloneRepo {\n      args,\n      git_token,\n      environment: repo.config.env_vars()?,\n      env_file_path,\n      on_clone,\n      on_pull,\n      skip_secret_interp: repo.config.skip_secret_interp,\n      replacers,\n    }\n    .resolve(&crate::api::Args)\n    .await\n    .map_err(|e| e.error)?\n  } else {\n    PullOrCloneRepo {\n      args,\n      git_token,\n      environment: repo.config.env_vars()?,\n      env_file_path,\n      on_clone,\n      on_pull,\n      skip_secret_interp: repo.config.skip_secret_interp,\n      replacers,\n    }\n    .resolve(&crate::api::Args)\n    .await\n    .map_err(|e| e.error)?\n  };\n\n  res.logs().extend(clone_res.res.logs);\n  res.set_commit_hash(clone_res.res.commit_hash);\n  res.set_commit_message(clone_res.res.commit_message);\n\n  if !all_logs_success(res.logs()) {\n    return Ok((root, None));\n  }\n\n  let run_directory = root\n    .join(&stack.config.run_directory)\n    .components()\n    .collect::<PathBuf>();\n\n  let env_file_path = environment::write_env_file(\n    &stack.config.env_vars()?,\n    run_directory.as_path(),\n    &stack.config.env_file_path,\n    res.logs(),\n  )\n  .await;\n  if !all_logs_success(res.logs()) {\n    return Err(anyhow!(\"Failed to write env file, stopping run\"));\n  }\n\n  Ok((\n    run_directory,\n    env_file_path\n      .is_some()\n      .then_some(&stack.config.env_file_path),\n  ))\n}\n\nasync fn write_stack_inline_repo(\n  stack: &Stack,\n  git_token: Option<String>,\n  mut res: impl WriteStackRes,\n) -> anyhow::Result<(\n  // run_directory\n  PathBuf,\n  // env_file_path\n  Option<&str>,\n)> {\n  let root = periphery_config()\n    .stack_dir()\n    .join(to_path_compatible_name(&stack.name))\n    .join(&stack.config.clone_path)\n    .components()\n    .collect::<PathBuf>();\n\n  let mut args: RepoExecutionArgs = stack.into();\n  // Set the clone destination to the one created for this run\n  args.destination = Some(root.display().to_string());\n\n  let git_token = stack_git_token(git_token, &args, &mut res)?;\n\n  let clone_res = if stack.config.reclone {\n    CloneRepo {\n      args,\n      git_token,\n      environment: Default::default(),\n      env_file_path: Default::default(),\n      on_clone: Default::default(),\n      on_pull: Default::default(),\n      skip_secret_interp: Default::default(),\n      replacers: Default::default(),\n    }\n    .resolve(&crate::api::Args)\n    .await\n    .map_err(|e| e.error)?\n  } else {\n    PullOrCloneRepo {\n      args,\n      git_token,\n      environment: Default::default(),\n      env_file_path: Default::default(),\n      on_clone: Default::default(),\n      on_pull: Default::default(),\n      skip_secret_interp: Default::default(),\n      replacers: Default::default(),\n    }\n    .resolve(&crate::api::Args)\n    .await\n    .map_err(|e| e.error)?\n  };\n\n  res.logs().extend(clone_res.res.logs);\n  res.set_commit_hash(clone_res.res.commit_hash);\n  res.set_commit_message(clone_res.res.commit_message);\n\n  if !all_logs_success(res.logs()) {\n    return Ok((root, None));\n  }\n\n  let run_directory = root\n    .join(&stack.config.run_directory)\n    .components()\n    .collect::<PathBuf>();\n\n  let env_file_path = environment::write_env_file(\n    &stack.config.env_vars()?,\n    run_directory.as_path(),\n    &stack.config.env_file_path,\n    res.logs(),\n  )\n  .await;\n  if !all_logs_success(res.logs()) {\n    return Err(anyhow!(\"Failed to write env file, stopping run\"));\n  }\n\n  Ok((\n    run_directory,\n    env_file_path\n      .is_some()\n      .then_some(&stack.config.env_file_path),\n  ))\n}\n\nasync fn write_stack_ui_defined(\n  stack: &Stack,\n  mut res: impl WriteStackRes,\n) -> anyhow::Result<(\n  // run_directory\n  PathBuf,\n  // env_file_path\n  Option<&str>,\n)> {\n  if stack.config.file_contents.trim().is_empty() {\n    return Err(anyhow!(\n      \"Must either input compose file contents directly, or use files on host / git repo options.\"\n    ));\n  }\n\n  let run_directory = periphery_config()\n    .stack_dir()\n    .join(to_path_compatible_name(&stack.name))\n    .components()\n    .collect::<PathBuf>();\n\n  // Ensure run directory exists\n  fs::create_dir_all(&run_directory).await.with_context(|| {\n    format!(\n      \"failed to create stack run directory at {run_directory:?}\"\n    )\n  })?;\n  let env_file_path = environment::write_env_file(\n    &stack.config.env_vars()?,\n    run_directory.as_path(),\n    &stack.config.env_file_path,\n    res.logs(),\n  )\n  .await;\n  if !all_logs_success(res.logs()) {\n    return Err(anyhow!(\"Failed to write env file, stopping run\"));\n  }\n\n  let file_path = run_directory\n    .join(\n      stack\n        .config\n        .file_paths\n        // only need the first one, or default\n        .first()\n        .map(String::as_str)\n        .unwrap_or(\"compose.yaml\"),\n    )\n    .components()\n    .collect::<PathBuf>();\n\n  fs::write(&file_path, &stack.config.file_contents)\n    .await\n    .with_context(|| {\n      format!(\"Failed to write compose file to {file_path:?}\")\n    })?;\n\n  Ok((\n    run_directory,\n    env_file_path\n      .is_some()\n      .then_some(&stack.config.env_file_path),\n  ))\n}\n\nfn stack_git_token<R: WriteStackRes>(\n  core_token: Option<String>,\n  args: &RepoExecutionArgs,\n  res: &mut R,\n) -> anyhow::Result<Option<String>> {\n  helpers::git_token(core_token, args).map_err(|e| {\n    let error = format_serror(&e.into());\n    res\n      .logs()\n      .push(Log::error(\"Missing git token\", error.clone()));\n    res.add_remote_error(FileContents {\n      path: Default::default(),\n      contents: error,\n    });\n    anyhow!(\"failed to find required git token, stopping run\")\n  })\n}\n"
  },
  {
    "path": "bin/periphery/src/config.rs",
    "content": "use std::{path::PathBuf, sync::OnceLock};\n\nuse clap::Parser;\nuse colored::Colorize;\nuse config::ConfigLoader;\nuse environment_file::maybe_read_list_from_file;\nuse komodo_client::entities::{\n  config::periphery::{CliArgs, Env, PeripheryConfig},\n  logger::{LogConfig, LogLevel},\n};\n\npub fn periphery_config() -> &'static PeripheryConfig {\n  static PERIPHERY_CONFIG: OnceLock<PeripheryConfig> =\n    OnceLock::new();\n  PERIPHERY_CONFIG.get_or_init(|| {\n    let env: Env = envy::from_env()\n      .expect(\"failed to parse periphery environment\");\n    let args = CliArgs::parse();\n    let config_paths =\n      args.config_path.unwrap_or(env.periphery_config_paths);\n\n    let config = if config_paths.is_empty() {\n      println!(\n        \"{}: No config paths found, using default config\",\n        \"INFO\".green(),\n      );\n      PeripheryConfig::default()\n    } else {\n      (ConfigLoader {\n        paths: &config_paths\n          .iter()\n          .map(PathBuf::as_path)\n          .collect::<Vec<_>>(),\n        match_wildcards: &args\n          .config_keyword\n          .unwrap_or(env.periphery_config_keywords)\n          .iter()\n          .map(String::as_str)\n          .collect::<Vec<_>>(),\n        include_file_name: \".peripheryinclude\",\n        merge_nested: args\n          .merge_nested_config\n          .unwrap_or(env.periphery_merge_nested_config),\n        extend_array: args\n          .extend_config_arrays\n          .unwrap_or(env.periphery_extend_config_arrays),\n        debug_print: args\n          .log_level\n          .map(|level| {\n            level == tracing::Level::DEBUG\n              || level == tracing::Level::TRACE\n          })\n          .unwrap_or_default(),\n      })\n      .load()\n      .expect(\"failed at parsing config from paths\")\n    };\n\n    PeripheryConfig {\n      port: env.periphery_port.unwrap_or(config.port),\n      bind_ip: env.periphery_bind_ip.unwrap_or(config.bind_ip),\n      root_directory: env\n        .periphery_root_directory\n        .unwrap_or(config.root_directory),\n      repo_dir: env.periphery_repo_dir.or(config.repo_dir),\n      stack_dir: env.periphery_stack_dir.or(config.stack_dir),\n      build_dir: env.periphery_build_dir.or(config.build_dir),\n      disable_terminals: env\n        .periphery_disable_terminals\n        .unwrap_or(config.disable_terminals),\n      disable_container_exec: env\n        .periphery_disable_container_exec\n        .unwrap_or(config.disable_container_exec),\n      stats_polling_rate: env\n        .periphery_stats_polling_rate\n        .unwrap_or(config.stats_polling_rate),\n      container_stats_polling_rate: env\n        .periphery_container_stats_polling_rate\n        .unwrap_or(config.container_stats_polling_rate),\n      legacy_compose_cli: env\n        .periphery_legacy_compose_cli\n        .unwrap_or(config.legacy_compose_cli),\n      logging: LogConfig {\n        level: args\n          .log_level\n          .map(LogLevel::from)\n          .or(env.periphery_logging_level)\n          .unwrap_or(config.logging.level),\n        stdio: env\n          .periphery_logging_stdio\n          .unwrap_or(config.logging.stdio),\n        pretty: env\n          .periphery_logging_pretty\n          .unwrap_or(config.logging.pretty),\n        location: env\n          .periphery_logging_location\n          .unwrap_or(config.logging.location),\n        otlp_endpoint: env\n          .periphery_logging_otlp_endpoint\n          .unwrap_or(config.logging.otlp_endpoint),\n        opentelemetry_service_name: env\n          .periphery_logging_opentelemetry_service_name\n          .unwrap_or(config.logging.opentelemetry_service_name),\n      },\n      pretty_startup_config: env\n        .periphery_pretty_startup_config\n        .unwrap_or(config.pretty_startup_config),\n      allowed_ips: env\n        .periphery_allowed_ips\n        .unwrap_or(config.allowed_ips),\n      passkeys: maybe_read_list_from_file(\n        env.periphery_passkeys_file,\n        env.periphery_passkeys,\n      )\n      .unwrap_or(config.passkeys),\n      include_disk_mounts: env\n        .periphery_include_disk_mounts\n        .unwrap_or(config.include_disk_mounts),\n      exclude_disk_mounts: env\n        .periphery_exclude_disk_mounts\n        .unwrap_or(config.exclude_disk_mounts),\n      ssl_enabled: env\n        .periphery_ssl_enabled\n        .unwrap_or(config.ssl_enabled),\n      ssl_key_file: env\n        .periphery_ssl_key_file\n        .or(config.ssl_key_file),\n      ssl_cert_file: env\n        .periphery_ssl_cert_file\n        .or(config.ssl_cert_file),\n      secrets: config.secrets,\n      git_providers: config.git_providers,\n      docker_registries: config.docker_registries,\n    }\n  })\n}\n"
  },
  {
    "path": "bin/periphery/src/docker/containers.rs",
    "content": "use std::collections::HashMap;\n\nuse anyhow::Context;\nuse bollard::query_parameters::{\n  InspectContainerOptions, ListContainersOptions,\n};\nuse komodo_client::entities::docker::{\n  ContainerConfig, GraphDriverData, HealthConfig, PortBinding,\n  container::*,\n};\n\nuse super::{DockerClient, stats::container_stats};\n\nimpl DockerClient {\n  pub async fn list_containers(\n    &self,\n  ) -> anyhow::Result<Vec<ContainerListItem>> {\n    let containers = self\n      .docker\n      .list_containers(Some(ListContainersOptions {\n        all: true,\n        ..Default::default()\n      }))\n      .await?;\n    let stats = container_stats().load();\n    let mut containers = containers\n      .into_iter()\n      .flat_map(|container| {\n        let name = container\n          .names\n          .context(\"no names on container\")?\n          .pop()\n          .context(\"no names on container (empty vec)\")?\n          .replace('/', \"\");\n        let stats = stats.get(&name).cloned();\n        anyhow::Ok(ContainerListItem {\n          server_id: None,\n          name,\n          stats,\n          id: container.id,\n          image: container.image,\n          image_id: container.image_id,\n          created: container.created,\n          size_rw: container.size_rw,\n          size_root_fs: container.size_root_fs,\n          state: convert_summary_container_state(\n            container.state.context(\"no container state\")?,\n          ),\n          status: container.status,\n          network_mode: container\n            .host_config\n            .and_then(|config| config.network_mode),\n          networks: container\n            .network_settings\n            .and_then(|settings| {\n              settings.networks.map(|networks| {\n                let mut keys =\n                  networks.into_keys().collect::<Vec<_>>();\n                keys.sort();\n                keys\n              })\n            })\n            .unwrap_or_default(),\n          ports: container\n            .ports\n            .map(|ports| {\n              ports.into_iter().map(convert_port).collect()\n            })\n            .unwrap_or_default(),\n          volumes: container\n            .mounts\n            .map(|settings| {\n              settings\n                .into_iter()\n                .filter_map(|mount| mount.name)\n                .collect()\n            })\n            .unwrap_or_default(),\n          labels: container.labels.unwrap_or_default(),\n        })\n      })\n      .collect::<Vec<_>>();\n    let container_id_to_network = containers\n      .iter()\n      .filter_map(|c| Some((c.id.clone()?, c.network_mode.clone()?)))\n      .collect::<HashMap<_, _>>();\n    // Fix containers which use `container:container_id` network_mode,\n    // by replacing with the referenced network mode.\n    containers.iter_mut().for_each(|container| {\n      let Some(network_name) = &container.network_mode else {\n        return;\n      };\n      let Some(container_id) =\n        network_name.strip_prefix(\"container:\")\n      else {\n        return;\n      };\n      container.network_mode =\n        container_id_to_network.get(container_id).cloned();\n    });\n    Ok(containers)\n  }\n\n  pub async fn inspect_container(\n    &self,\n    container_name: &str,\n  ) -> anyhow::Result<Container> {\n    let container = self\n      .docker\n      .inspect_container(\n        container_name,\n        InspectContainerOptions { size: true }.into(),\n      )\n      .await?;\n    Ok(Container {\n      id: container.id,\n      created: container.created,\n      path: container.path,\n      args: container.args.unwrap_or_default(),\n      state: container.state.map(|state| ContainerState {\n        status: state\n          .status\n          .map(convert_container_state_status)\n          .unwrap_or_default(),\n        running: state.running,\n        paused: state.paused,\n        restarting: state.restarting,\n        oom_killed: state.oom_killed,\n        dead: state.dead,\n        pid: state.pid,\n        exit_code: state.exit_code,\n        error: state.error,\n        started_at: state.started_at,\n        finished_at: state.finished_at,\n        health: state.health.map(|health| ContainerHealth {\n          status: health\n            .status\n            .map(convert_health_status)\n            .unwrap_or_default(),\n          failing_streak: health.failing_streak,\n          log: health\n            .log\n            .map(|log| {\n              log\n                .into_iter()\n                .map(convert_health_check_result)\n                .collect()\n            })\n            .unwrap_or_default(),\n        }),\n      }),\n      image: container.image,\n      resolv_conf_path: container.resolv_conf_path,\n      hostname_path: container.hostname_path,\n      hosts_path: container.hosts_path,\n      log_path: container.log_path,\n      name: container.name,\n      restart_count: container.restart_count,\n      driver: container.driver,\n      platform: container.platform,\n      mount_label: container.mount_label,\n      process_label: container.process_label,\n      app_armor_profile: container.app_armor_profile,\n      exec_ids: container.exec_ids.unwrap_or_default(),\n      host_config: container.host_config.map(|config| HostConfig {\n        cpu_shares: config.cpu_shares,\n        memory: config.memory,\n        cgroup_parent: config.cgroup_parent,\n        blkio_weight: config.blkio_weight,\n        blkio_weight_device: config\n          .blkio_weight_device\n          .unwrap_or_default()\n          .into_iter()\n          .map(|device| ResourcesBlkioWeightDevice {\n            path: device.path,\n            weight: device.weight,\n          })\n          .collect(),\n        blkio_device_read_bps: config\n          .blkio_device_read_bps\n          .unwrap_or_default()\n          .into_iter()\n          .map(|bp| ThrottleDevice {\n            path: bp.path,\n            rate: bp.rate,\n          })\n          .collect(),\n        blkio_device_write_bps: config\n          .blkio_device_write_bps\n          .unwrap_or_default()\n          .into_iter()\n          .map(|bp| ThrottleDevice {\n            path: bp.path,\n            rate: bp.rate,\n          })\n          .collect(),\n        blkio_device_read_iops: config\n          .blkio_device_read_iops\n          .unwrap_or_default()\n          .into_iter()\n          .map(|iops| ThrottleDevice {\n            path: iops.path,\n            rate: iops.rate,\n          })\n          .collect(),\n        blkio_device_write_iops: config\n          .blkio_device_write_iops\n          .unwrap_or_default()\n          .into_iter()\n          .map(|iops| ThrottleDevice {\n            path: iops.path,\n            rate: iops.rate,\n          })\n          .collect(),\n        cpu_period: config.cpu_period,\n        cpu_quota: config.cpu_quota,\n        cpu_realtime_period: config.cpu_realtime_period,\n        cpu_realtime_runtime: config.cpu_realtime_runtime,\n        cpuset_cpus: config.cpuset_cpus,\n        cpuset_mems: config.cpuset_mems,\n        devices: config\n          .devices\n          .unwrap_or_default()\n          .into_iter()\n          .map(|device| DeviceMapping {\n            path_on_host: device.path_on_host,\n            path_in_container: device.path_in_container,\n            cgroup_permissions: device.cgroup_permissions,\n          })\n          .collect(),\n        device_cgroup_rules: config\n          .device_cgroup_rules\n          .unwrap_or_default(),\n        device_requests: config\n          .device_requests\n          .unwrap_or_default()\n          .into_iter()\n          .map(|request| DeviceRequest {\n            driver: request.driver,\n            count: request.count,\n            device_ids: request.device_ids.unwrap_or_default(),\n            capabilities: request.capabilities.unwrap_or_default(),\n            options: request.options.unwrap_or_default(),\n          })\n          .collect(),\n        kernel_memory_tcp: config.kernel_memory_tcp,\n        memory_reservation: config.memory_reservation,\n        memory_swap: config.memory_swap,\n        memory_swappiness: config.memory_swappiness,\n        nano_cpus: config.nano_cpus,\n        oom_kill_disable: config.oom_kill_disable,\n        init: config.init,\n        pids_limit: config.pids_limit,\n        ulimits: config\n          .ulimits\n          .unwrap_or_default()\n          .into_iter()\n          .map(|ulimit| ResourcesUlimits {\n            name: ulimit.name,\n            soft: ulimit.soft,\n            hard: ulimit.hard,\n          })\n          .collect(),\n        cpu_count: config.cpu_count,\n        cpu_percent: config.cpu_percent,\n        io_maximum_iops: config.io_maximum_iops,\n        io_maximum_bandwidth: config.io_maximum_bandwidth,\n        binds: config.binds.unwrap_or_default(),\n        container_id_file: config.container_id_file,\n        log_config: config.log_config.map(|config| {\n          HostConfigLogConfig {\n            typ: config.typ,\n            config: config.config.unwrap_or_default(),\n          }\n        }),\n        network_mode: config.network_mode,\n        port_bindings: config\n          .port_bindings\n          .unwrap_or_default()\n          .into_iter()\n          .map(|(k, v)| {\n            (\n              k,\n              v.unwrap_or_default()\n                .into_iter()\n                .map(|v| PortBinding {\n                  host_ip: v.host_ip,\n                  host_port: v.host_port,\n                })\n                .collect(),\n            )\n          })\n          .collect(),\n        restart_policy: config.restart_policy.map(|policy| {\n          RestartPolicy {\n            name: policy\n              .name\n              .map(convert_restart_policy)\n              .unwrap_or_default(),\n            maximum_retry_count: policy.maximum_retry_count,\n          }\n        }),\n        auto_remove: config.auto_remove,\n        volume_driver: config.volume_driver,\n        volumes_from: config.volumes_from.unwrap_or_default(),\n        mounts: config\n          .mounts\n          .unwrap_or_default()\n          .into_iter()\n          .map(|mount| ContainerMount {\n            target: mount.target,\n            source: mount.source,\n            typ: mount\n              .typ\n              .map(convert_mount_type)\n              .unwrap_or_default(),\n            read_only: mount.read_only,\n            consistency: mount.consistency,\n            bind_options: mount.bind_options.map(|options| {\n              MountBindOptions {\n                propagation: options\n                  .propagation\n                  .map(convert_mount_propogation)\n                  .unwrap_or_default(),\n                non_recursive: options.non_recursive,\n                create_mountpoint: options.create_mountpoint,\n                read_only_non_recursive: options\n                  .read_only_non_recursive,\n                read_only_force_recursive: options\n                  .read_only_force_recursive,\n              }\n            }),\n            volume_options: mount.volume_options.map(|options| {\n              MountVolumeOptions {\n                no_copy: options.no_copy,\n                labels: options.labels.unwrap_or_default(),\n                driver_config: options.driver_config.map(|config| {\n                  MountVolumeOptionsDriverConfig {\n                    name: config.name,\n                    options: config.options.unwrap_or_default(),\n                  }\n                }),\n                subpath: options.subpath,\n              }\n            }),\n            tmpfs_options: mount.tmpfs_options.map(|options| {\n              MountTmpfsOptions {\n                size_bytes: options.size_bytes,\n                mode: options.mode,\n              }\n            }),\n          })\n          .collect(),\n        console_size: config\n          .console_size\n          .map(|v| v.into_iter().map(|s| s as i32).collect())\n          .unwrap_or_default(),\n        annotations: config.annotations.unwrap_or_default(),\n        cap_add: config.cap_add.unwrap_or_default(),\n        cap_drop: config.cap_drop.unwrap_or_default(),\n        cgroupns_mode: config\n          .cgroupns_mode\n          .map(convert_cgroupns_mode),\n        dns: config.dns.unwrap_or_default(),\n        dns_options: config.dns_options.unwrap_or_default(),\n        dns_search: config.dns_search.unwrap_or_default(),\n        extra_hosts: config.extra_hosts.unwrap_or_default(),\n        group_add: config.group_add.unwrap_or_default(),\n        ipc_mode: config.ipc_mode,\n        cgroup: config.cgroup,\n        links: config.links.unwrap_or_default(),\n        oom_score_adj: config.oom_score_adj,\n        pid_mode: config.pid_mode,\n        privileged: config.privileged,\n        publish_all_ports: config.publish_all_ports,\n        readonly_rootfs: config.readonly_rootfs,\n        security_opt: config.security_opt.unwrap_or_default(),\n        storage_opt: config.storage_opt.unwrap_or_default(),\n        tmpfs: config.tmpfs.unwrap_or_default(),\n        uts_mode: config.uts_mode,\n        userns_mode: config.userns_mode,\n        shm_size: config.shm_size,\n        sysctls: config.sysctls.unwrap_or_default(),\n        runtime: config.runtime,\n        isolation: config\n          .isolation\n          .map(convert_isolation_mode)\n          .unwrap_or_default(),\n        masked_paths: config.masked_paths.unwrap_or_default(),\n        readonly_paths: config.readonly_paths.unwrap_or_default(),\n      }),\n      graph_driver: container.graph_driver.map(|driver| {\n        GraphDriverData {\n          name: driver.name,\n          data: driver.data,\n        }\n      }),\n      size_rw: container.size_rw,\n      size_root_fs: container.size_root_fs,\n      mounts: container\n        .mounts\n        .unwrap_or_default()\n        .into_iter()\n        .map(|mount| MountPoint {\n          typ: mount\n            .typ\n            .map(convert_mount_point_type)\n            .unwrap_or_default(),\n          name: mount.name,\n          source: mount.source,\n          destination: mount.destination,\n          driver: mount.driver,\n          mode: mount.mode,\n          rw: mount.rw,\n          propagation: mount.propagation,\n        })\n        .collect(),\n      config: container.config.map(|config| ContainerConfig {\n        hostname: config.hostname,\n        domainname: config.domainname,\n        user: config.user,\n        attach_stdin: config.attach_stdin,\n        attach_stdout: config.attach_stdout,\n        attach_stderr: config.attach_stderr,\n        exposed_ports: config\n          .exposed_ports\n          .unwrap_or_default()\n          .into_keys()\n          .map(|k| (k, Default::default()))\n          .collect(),\n        tty: config.tty,\n        open_stdin: config.open_stdin,\n        stdin_once: config.stdin_once,\n        env: config.env.unwrap_or_default(),\n        cmd: config.cmd.unwrap_or_default(),\n        healthcheck: config.healthcheck.map(|health| HealthConfig {\n          test: health.test.unwrap_or_default(),\n          interval: health.interval,\n          timeout: health.timeout,\n          retries: health.retries,\n          start_period: health.start_period,\n          start_interval: health.start_interval,\n        }),\n        args_escaped: config.args_escaped,\n        image: config.image,\n        volumes: config\n          .volumes\n          .unwrap_or_default()\n          .into_keys()\n          .map(|k| (k, Default::default()))\n          .collect(),\n        working_dir: config.working_dir,\n        entrypoint: config.entrypoint.unwrap_or_default(),\n        network_disabled: config.network_disabled,\n        mac_address: config.mac_address,\n        on_build: config.on_build.unwrap_or_default(),\n        labels: config.labels.unwrap_or_default(),\n        stop_signal: config.stop_signal,\n        stop_timeout: config.stop_timeout,\n        shell: config.shell.unwrap_or_default(),\n      }),\n      network_settings: container.network_settings.map(|settings| {\n        NetworkSettings {\n          bridge: settings.bridge,\n          sandbox_id: settings.sandbox_id,\n          ports: settings\n            .ports\n            .unwrap_or_default()\n            .into_iter()\n            .map(|(k, v)| {\n              (\n                k,\n                v.unwrap_or_default()\n                  .into_iter()\n                  .map(|v| PortBinding {\n                    host_ip: v.host_ip,\n                    host_port: v.host_port,\n                  })\n                  .collect(),\n              )\n            })\n            .collect(),\n          sandbox_key: settings.sandbox_key,\n          networks: settings\n            .networks\n            .unwrap_or_default()\n            .into_iter()\n            .map(|(k, v)| {\n              (\n                k,\n                EndpointSettings {\n                  ipam_config: v.ipam_config.map(|ipam| {\n                    EndpointIpamConfig {\n                      ipv4_address: ipam.ipv4_address,\n                      ipv6_address: ipam.ipv6_address,\n                      link_local_ips: ipam\n                        .link_local_ips\n                        .unwrap_or_default(),\n                    }\n                  }),\n                  links: v.links.unwrap_or_default(),\n                  mac_address: v.mac_address,\n                  aliases: v.aliases.unwrap_or_default(),\n                  network_id: v.network_id,\n                  endpoint_id: v.endpoint_id,\n                  gateway: v.gateway,\n                  ip_address: v.ip_address,\n                  ip_prefix_len: v.ip_prefix_len,\n                  ipv6_gateway: v.ipv6_gateway,\n                  global_ipv6_address: v.global_ipv6_address,\n                  global_ipv6_prefix_len: v.global_ipv6_prefix_len,\n                  driver_opts: v.driver_opts.unwrap_or_default(),\n                  dns_names: v.dns_names.unwrap_or_default(),\n                },\n              )\n            })\n            .collect(),\n        }\n      }),\n    })\n  }\n}\n\nfn convert_summary_container_state(\n  state: bollard::secret::ContainerSummaryStateEnum,\n) -> ContainerStateStatusEnum {\n  match state {\n    bollard::secret::ContainerSummaryStateEnum::EMPTY => {\n      ContainerStateStatusEnum::Empty\n    }\n    bollard::secret::ContainerSummaryStateEnum::CREATED => {\n      ContainerStateStatusEnum::Created\n    }\n    bollard::secret::ContainerSummaryStateEnum::RUNNING => {\n      ContainerStateStatusEnum::Running\n    }\n    bollard::secret::ContainerSummaryStateEnum::PAUSED => {\n      ContainerStateStatusEnum::Paused\n    }\n    bollard::secret::ContainerSummaryStateEnum::RESTARTING => {\n      ContainerStateStatusEnum::Restarting\n    }\n    bollard::secret::ContainerSummaryStateEnum::EXITED => {\n      ContainerStateStatusEnum::Exited\n    }\n    bollard::secret::ContainerSummaryStateEnum::REMOVING => {\n      ContainerStateStatusEnum::Removing\n    }\n    bollard::secret::ContainerSummaryStateEnum::DEAD => {\n      ContainerStateStatusEnum::Dead\n    }\n  }\n}\n\nfn convert_container_state_status(\n  state: bollard::secret::ContainerStateStatusEnum,\n) -> ContainerStateStatusEnum {\n  match state {\n    bollard::secret::ContainerStateStatusEnum::EMPTY => {\n      ContainerStateStatusEnum::Empty\n    }\n    bollard::secret::ContainerStateStatusEnum::CREATED => {\n      ContainerStateStatusEnum::Created\n    }\n    bollard::secret::ContainerStateStatusEnum::RUNNING => {\n      ContainerStateStatusEnum::Running\n    }\n    bollard::secret::ContainerStateStatusEnum::PAUSED => {\n      ContainerStateStatusEnum::Paused\n    }\n    bollard::secret::ContainerStateStatusEnum::RESTARTING => {\n      ContainerStateStatusEnum::Restarting\n    }\n    bollard::secret::ContainerStateStatusEnum::EXITED => {\n      ContainerStateStatusEnum::Exited\n    }\n    bollard::secret::ContainerStateStatusEnum::REMOVING => {\n      ContainerStateStatusEnum::Removing\n    }\n    bollard::secret::ContainerStateStatusEnum::DEAD => {\n      ContainerStateStatusEnum::Dead\n    }\n  }\n}\n\nfn convert_port_type(\n  typ: bollard::secret::PortTypeEnum,\n) -> PortTypeEnum {\n  match typ {\n    bollard::secret::PortTypeEnum::EMPTY => PortTypeEnum::EMPTY,\n    bollard::secret::PortTypeEnum::TCP => PortTypeEnum::TCP,\n    bollard::secret::PortTypeEnum::UDP => PortTypeEnum::UDP,\n    bollard::secret::PortTypeEnum::SCTP => PortTypeEnum::SCTP,\n  }\n}\nfn convert_port(port: bollard::secret::Port) -> Port {\n  Port {\n    ip: port.ip,\n    private_port: port.private_port,\n    public_port: port.public_port,\n    typ: port.typ.map(convert_port_type).unwrap_or_default(),\n  }\n}\n\nfn convert_health_status(\n  status: bollard::secret::HealthStatusEnum,\n) -> HealthStatusEnum {\n  match status {\n    bollard::secret::HealthStatusEnum::EMPTY => {\n      HealthStatusEnum::Empty\n    }\n    bollard::secret::HealthStatusEnum::NONE => HealthStatusEnum::None,\n    bollard::secret::HealthStatusEnum::STARTING => {\n      HealthStatusEnum::Starting\n    }\n    bollard::secret::HealthStatusEnum::HEALTHY => {\n      HealthStatusEnum::Healthy\n    }\n    bollard::secret::HealthStatusEnum::UNHEALTHY => {\n      HealthStatusEnum::Unhealthy\n    }\n  }\n}\n\nfn convert_health_check_result(\n  check: bollard::secret::HealthcheckResult,\n) -> HealthcheckResult {\n  HealthcheckResult {\n    start: check.start,\n    end: check.end,\n    exit_code: check.exit_code,\n    output: check.output,\n  }\n}\n\nfn convert_restart_policy(\n  policy: bollard::secret::RestartPolicyNameEnum,\n) -> RestartPolicyNameEnum {\n  match policy {\n    bollard::secret::RestartPolicyNameEnum::EMPTY => {\n      RestartPolicyNameEnum::Empty\n    }\n    bollard::secret::RestartPolicyNameEnum::NO => {\n      RestartPolicyNameEnum::No\n    }\n    bollard::secret::RestartPolicyNameEnum::ALWAYS => {\n      RestartPolicyNameEnum::Always\n    }\n    bollard::secret::RestartPolicyNameEnum::UNLESS_STOPPED => {\n      RestartPolicyNameEnum::UnlessStopped\n    }\n    bollard::secret::RestartPolicyNameEnum::ON_FAILURE => {\n      RestartPolicyNameEnum::OnFailure\n    }\n  }\n}\n\nfn convert_mount_type(\n  typ: bollard::secret::MountTypeEnum,\n) -> MountTypeEnum {\n  match typ {\n    bollard::secret::MountTypeEnum::EMPTY => MountTypeEnum::Empty,\n    bollard::secret::MountTypeEnum::BIND => MountTypeEnum::Bind,\n    bollard::secret::MountTypeEnum::VOLUME => MountTypeEnum::Volume,\n    bollard::secret::MountTypeEnum::IMAGE => MountTypeEnum::Image,\n    bollard::secret::MountTypeEnum::TMPFS => MountTypeEnum::Tmpfs,\n    bollard::secret::MountTypeEnum::NPIPE => MountTypeEnum::Npipe,\n    bollard::secret::MountTypeEnum::CLUSTER => MountTypeEnum::Cluster,\n  }\n}\n\nfn convert_mount_point_type(\n  typ: bollard::secret::MountPointTypeEnum,\n) -> MountTypeEnum {\n  match typ {\n    bollard::secret::MountPointTypeEnum::EMPTY => {\n      MountTypeEnum::Empty\n    }\n    bollard::secret::MountPointTypeEnum::BIND => MountTypeEnum::Bind,\n    bollard::secret::MountPointTypeEnum::VOLUME => {\n      MountTypeEnum::Volume\n    }\n    bollard::secret::MountPointTypeEnum::IMAGE => {\n      MountTypeEnum::Image\n    }\n    bollard::secret::MountPointTypeEnum::TMPFS => {\n      MountTypeEnum::Tmpfs\n    }\n    bollard::secret::MountPointTypeEnum::NPIPE => {\n      MountTypeEnum::Npipe\n    }\n    bollard::secret::MountPointTypeEnum::CLUSTER => {\n      MountTypeEnum::Cluster\n    }\n  }\n}\n\nfn convert_mount_propogation(\n  propogation: bollard::secret::MountBindOptionsPropagationEnum,\n) -> MountBindOptionsPropagationEnum {\n  match propogation {\n    bollard::secret::MountBindOptionsPropagationEnum::EMPTY => {\n      MountBindOptionsPropagationEnum::Empty\n    }\n    bollard::secret::MountBindOptionsPropagationEnum::PRIVATE => {\n      MountBindOptionsPropagationEnum::Private\n    }\n    bollard::secret::MountBindOptionsPropagationEnum::RPRIVATE => {\n      MountBindOptionsPropagationEnum::Rprivate\n    }\n    bollard::secret::MountBindOptionsPropagationEnum::SHARED => {\n      MountBindOptionsPropagationEnum::Shared\n    }\n    bollard::secret::MountBindOptionsPropagationEnum::RSHARED => {\n      MountBindOptionsPropagationEnum::Rshared\n    }\n    bollard::secret::MountBindOptionsPropagationEnum::SLAVE => {\n      MountBindOptionsPropagationEnum::Slave\n    }\n    bollard::secret::MountBindOptionsPropagationEnum::RSLAVE => {\n      MountBindOptionsPropagationEnum::Rslave\n    }\n  }\n}\n\nfn convert_cgroupns_mode(\n  mode: bollard::secret::HostConfigCgroupnsModeEnum,\n) -> HostConfigCgroupnsModeEnum {\n  match mode {\n    bollard::secret::HostConfigCgroupnsModeEnum::EMPTY => {\n      HostConfigCgroupnsModeEnum::Empty\n    }\n    bollard::secret::HostConfigCgroupnsModeEnum::PRIVATE => {\n      HostConfigCgroupnsModeEnum::Private\n    }\n    bollard::secret::HostConfigCgroupnsModeEnum::HOST => {\n      HostConfigCgroupnsModeEnum::Host\n    }\n  }\n}\n\nfn convert_isolation_mode(\n  isolation: bollard::secret::HostConfigIsolationEnum,\n) -> HostConfigIsolationEnum {\n  match isolation {\n    bollard::secret::HostConfigIsolationEnum::EMPTY => {\n      HostConfigIsolationEnum::Empty\n    }\n    bollard::secret::HostConfigIsolationEnum::DEFAULT => {\n      HostConfigIsolationEnum::Default\n    }\n    bollard::secret::HostConfigIsolationEnum::PROCESS => {\n      HostConfigIsolationEnum::Process\n    }\n    bollard::secret::HostConfigIsolationEnum::HYPERV => {\n      HostConfigIsolationEnum::Hyperv\n    }\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/docker/images.rs",
    "content": "use bollard::query_parameters::ListImagesOptions;\nuse komodo_client::entities::docker::{\n  ContainerConfig, GraphDriverData, HealthConfig,\n  container::ContainerListItem, image::*,\n};\n\nuse super::DockerClient;\n\nimpl DockerClient {\n  pub async fn list_images(\n    &self,\n    containers: &[ContainerListItem],\n  ) -> anyhow::Result<Vec<ImageListItem>> {\n    let images = self\n      .docker\n      .list_images(Option::<ListImagesOptions>::None)\n      .await?\n      .into_iter()\n      .map(|image| {\n        let in_use = containers.iter().any(|container| {\n          container\n            .image_id\n            .as_ref()\n            .map(|id| id == &image.id)\n            .unwrap_or_default()\n        });\n        ImageListItem {\n          name: image\n            .repo_tags\n            .into_iter()\n            .next()\n            .unwrap_or_else(|| image.id.clone()),\n          id: image.id,\n          parent_id: image.parent_id,\n          created: image.created,\n          size: image.size,\n          in_use,\n        }\n      })\n      .collect();\n    Ok(images)\n  }\n\n  pub async fn inspect_image(\n    &self,\n    image_name: &str,\n  ) -> anyhow::Result<Image> {\n    let image = self.docker.inspect_image(image_name).await?;\n    Ok(Image {\n      id: image.id,\n      repo_tags: image.repo_tags.unwrap_or_default(),\n      repo_digests: image.repo_digests.unwrap_or_default(),\n      parent: image.parent,\n      comment: image.comment,\n      created: image.created,\n      docker_version: image.docker_version,\n      author: image.author,\n      architecture: image.architecture,\n      variant: image.variant,\n      os: image.os,\n      os_version: image.os_version,\n      size: image.size,\n      graph_driver: image.graph_driver.map(|driver| {\n        GraphDriverData {\n          name: driver.name,\n          data: driver.data,\n        }\n      }),\n      root_fs: image.root_fs.map(|fs| ImageInspectRootFs {\n        typ: fs.typ,\n        layers: fs.layers.unwrap_or_default(),\n      }),\n      metadata: image.metadata.map(|metadata| ImageInspectMetadata {\n        last_tag_time: metadata.last_tag_time,\n      }),\n      config: image.config.map(|config| ContainerConfig {\n        hostname: config.hostname,\n        domainname: config.domainname,\n        user: config.user,\n        attach_stdin: config.attach_stdin,\n        attach_stdout: config.attach_stdout,\n        attach_stderr: config.attach_stderr,\n        exposed_ports: config\n          .exposed_ports\n          .unwrap_or_default()\n          .into_keys()\n          .map(|k| (k, Default::default()))\n          .collect(),\n        tty: config.tty,\n        open_stdin: config.open_stdin,\n        stdin_once: config.stdin_once,\n        env: config.env.unwrap_or_default(),\n        cmd: config.cmd.unwrap_or_default(),\n        healthcheck: config.healthcheck.map(|health| HealthConfig {\n          test: health.test.unwrap_or_default(),\n          interval: health.interval,\n          timeout: health.timeout,\n          retries: health.retries,\n          start_period: health.start_period,\n          start_interval: health.start_interval,\n        }),\n        args_escaped: config.args_escaped,\n        image: config.image,\n        volumes: config\n          .volumes\n          .unwrap_or_default()\n          .into_keys()\n          .map(|k| (k, Default::default()))\n          .collect(),\n        working_dir: config.working_dir,\n        entrypoint: config.entrypoint.unwrap_or_default(),\n        network_disabled: config.network_disabled,\n        mac_address: config.mac_address,\n        on_build: config.on_build.unwrap_or_default(),\n        labels: config.labels.unwrap_or_default(),\n        stop_signal: config.stop_signal,\n        stop_timeout: config.stop_timeout,\n        shell: config.shell.unwrap_or_default(),\n      }),\n    })\n  }\n\n  pub async fn image_history(\n    &self,\n    image_name: &str,\n  ) -> anyhow::Result<Vec<ImageHistoryResponseItem>> {\n    let res = self\n      .docker\n      .image_history(image_name)\n      .await?\n      .into_iter()\n      .map(|image| ImageHistoryResponseItem {\n        id: image.id,\n        created: image.created,\n        created_by: image.created_by,\n        tags: image.tags,\n        size: image.size,\n        comment: image.comment,\n      })\n      .collect();\n    Ok(res)\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/docker/mod.rs",
    "content": "use std::sync::OnceLock;\n\nuse anyhow::anyhow;\nuse bollard::Docker;\nuse command::run_komodo_command;\nuse komodo_client::entities::{TerminationSignal, update::Log};\nuse run_command::async_run_command;\n\npub mod stats;\n\nmod containers;\nmod images;\nmod networks;\nmod volumes;\n\npub fn docker_client() -> &'static DockerClient {\n  static DOCKER_CLIENT: OnceLock<DockerClient> = OnceLock::new();\n  DOCKER_CLIENT.get_or_init(Default::default)\n}\n\npub struct DockerClient {\n  docker: Docker,\n}\n\nimpl Default for DockerClient {\n  fn default() -> DockerClient {\n    DockerClient {\n      docker: Docker::connect_with_defaults()\n        .expect(\"failed to connect to docker daemon\"),\n    }\n  }\n}\n\n/// Returns whether build result should be pushed after build\n#[instrument(skip(registry_token))]\npub async fn docker_login(\n  domain: &str,\n  account: &str,\n  // For local token override from core.\n  registry_token: Option<&str>,\n) -> anyhow::Result<bool> {\n  if domain.is_empty() || account.is_empty() {\n    return Ok(false);\n  }\n  let registry_token = match registry_token {\n    Some(token) => token,\n    None => crate::helpers::registry_token(domain, account)?,\n  };\n  let log = async_run_command(&format!(\n    \"echo {registry_token} | docker login {domain} --username '{account}' --password-stdin\",\n  ))\n  .await;\n  if log.success() {\n    Ok(true)\n  } else {\n    let mut e = anyhow!(\"End of trace\");\n    for line in\n      log.stderr.split('\\n').filter(|line| !line.is_empty()).rev()\n    {\n      e = e.context(line.to_string());\n    }\n    for line in\n      log.stdout.split('\\n').filter(|line| !line.is_empty()).rev()\n    {\n      e = e.context(line.to_string());\n    }\n    Err(e.context(format!(\"Registry {domain} login error\")))\n  }\n}\n\n#[instrument]\npub async fn pull_image(image: &str) -> Log {\n  let command = format!(\"docker pull {image}\");\n  run_komodo_command(\"Docker Pull\", None, command).await\n}\n\npub fn stop_container_command(\n  container_name: &str,\n  signal: Option<TerminationSignal>,\n  time: Option<i32>,\n) -> String {\n  let signal = signal\n    .map(|signal| format!(\" --signal {signal}\"))\n    .unwrap_or_default();\n  let time = time\n    .map(|time| format!(\" --time {time}\"))\n    .unwrap_or_default();\n  format!(\"docker stop{signal}{time} {container_name}\")\n}\n"
  },
  {
    "path": "bin/periphery/src/docker/networks.rs",
    "content": "use bollard::query_parameters::{\n  InspectNetworkOptions, ListNetworksOptions,\n};\nuse komodo_client::entities::docker::{\n  container::ContainerListItem, network::*,\n};\n\nuse super::DockerClient;\n\nimpl DockerClient {\n  pub async fn list_networks(\n    &self,\n    containers: &[ContainerListItem],\n  ) -> anyhow::Result<Vec<NetworkListItem>> {\n    let networks = self\n      .docker\n      .list_networks(Option::<ListNetworksOptions>::None)\n      .await?\n      .into_iter()\n      .map(|network| {\n        let (ipam_driver, ipam_subnet, ipam_gateway) =\n          if let Some(ipam) = network.ipam {\n            let (subnet, gateway) = if let Some(config) = ipam\n              .config\n              .and_then(|configs| configs.into_iter().next())\n            {\n              (config.subnet, config.gateway)\n            } else {\n              (None, None)\n            };\n            (ipam.driver, subnet, gateway)\n          } else {\n            (None, None, None)\n          };\n        let in_use = match &network.name {\n          Some(name) => containers.iter().any(|container| {\n            container.networks.iter().any(|_name| name == _name)\n          }),\n          None => false,\n        };\n        NetworkListItem {\n          name: network.name,\n          id: network.id,\n          created: network.created,\n          scope: network.scope,\n          driver: network.driver,\n          enable_ipv6: network.enable_ipv6,\n          ipam_driver,\n          ipam_subnet,\n          ipam_gateway,\n          internal: network.internal,\n          attachable: network.attachable,\n          ingress: network.ingress,\n          in_use,\n        }\n      })\n      .collect();\n    Ok(networks)\n  }\n\n  pub async fn inspect_network(\n    &self,\n    network_name: &str,\n  ) -> anyhow::Result<Network> {\n    let network = self\n      .docker\n      .inspect_network(\n        network_name,\n        InspectNetworkOptions {\n          verbose: true,\n          ..Default::default()\n        }\n        .into(),\n      )\n      .await?;\n    Ok(Network {\n      name: network.name,\n      id: network.id,\n      created: network.created,\n      scope: network.scope,\n      driver: network.driver,\n      enable_ipv6: network.enable_ipv6,\n      ipam: network.ipam.map(|ipam| Ipam {\n        driver: ipam.driver,\n        config: ipam\n          .config\n          .unwrap_or_default()\n          .into_iter()\n          .map(|config| IpamConfig {\n            subnet: config.subnet,\n            ip_range: config.ip_range,\n            gateway: config.gateway,\n            auxiliary_addresses: config\n              .auxiliary_addresses\n              .unwrap_or_default(),\n          })\n          .collect(),\n        options: ipam.options.unwrap_or_default(),\n      }),\n      internal: network.internal,\n      attachable: network.attachable,\n      ingress: network.ingress,\n      containers: network\n        .containers\n        .unwrap_or_default()\n        .into_iter()\n        .map(|(container_id, container)| NetworkContainer {\n          container_id,\n          name: container.name,\n          endpoint_id: container.endpoint_id,\n          mac_address: container.mac_address,\n          ipv4_address: container.ipv4_address,\n          ipv6_address: container.ipv6_address,\n        })\n        .collect(),\n      options: network.options.unwrap_or_default(),\n      labels: network.labels.unwrap_or_default(),\n    })\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/docker/stats.rs",
    "content": "use std::{\n  collections::HashMap,\n  sync::{Arc, OnceLock},\n};\n\nuse anyhow::{Context, anyhow};\nuse arc_swap::ArcSwap;\nuse async_timing_util::wait_until_timelength;\nuse bollard::{models, query_parameters::StatsOptionsBuilder};\nuse futures::StreamExt;\nuse komodo_client::entities::docker::{\n  container::ContainerStats,\n  stats::{\n    ContainerBlkioStatEntry, ContainerBlkioStats, ContainerCpuStats,\n    ContainerCpuUsage, ContainerMemoryStats, ContainerNetworkStats,\n    ContainerPidsStats, ContainerStorageStats,\n    ContainerThrottlingData, FullContainerStats,\n  },\n};\nuse run_command::async_run_command;\n\nuse crate::{config::periphery_config, docker::DockerClient};\n\npub type ContainerStatsMap = HashMap<String, ContainerStats>;\n\npub fn container_stats() -> &'static ArcSwap<ContainerStatsMap> {\n  static CONTAINER_STATS: OnceLock<ArcSwap<ContainerStatsMap>> =\n    OnceLock::new();\n  CONTAINER_STATS.get_or_init(Default::default)\n}\n\npub fn spawn_polling_thread() {\n  tokio::spawn(async move {\n    let polling_rate = periphery_config()\n      .container_stats_polling_rate\n      .to_string()\n      .parse()\n      .expect(\"invalid stats polling rate\");\n    update_container_stats().await;\n    loop {\n      let _ts = wait_until_timelength(polling_rate, 200).await;\n      update_container_stats().await;\n    }\n  });\n}\n\nasync fn update_container_stats() {\n  match get_container_stats(None).await {\n    Ok(stats) => {\n      container_stats().store(Arc::new(\n        stats.into_iter().map(|s| (s.name.clone(), s)).collect(),\n      ));\n    }\n    Err(e) => {\n      error!(\"Failed to refresh container stats cache | {e:#}\");\n    }\n  }\n}\n\npub async fn get_container_stats(\n  container_name: Option<String>,\n) -> anyhow::Result<Vec<ContainerStats>> {\n  let format = \"--format '{\\\"BlockIO\\\":\\\"{{ .BlockIO }}\\\", \\\n                           \\\"CPUPerc\\\":\\\"{{ .CPUPerc }}\\\", \\\n                           \\\"ID\\\":\\\"{{ .ID }}\\\", \\\n                           \\\"MemPerc\\\":\\\"{{ .MemPerc }}\\\", \\\n                           \\\"MemUsage\\\":\\\"{{ .MemUsage }}\\\", \\\n                           \\\"Name\\\":\\\"{{ .Name }}\\\", \\\n                           \\\"NetIO\\\":\\\"{{ .NetIO }}\\\",\\\n                           \\\"PIDs\\\":\\\"{{ .PIDs }}\\\"}'\";\n  let container_name = match container_name {\n    Some(name) => format!(\" {name}\"),\n    None => \"\".to_string(),\n  };\n  let command =\n    format!(\"docker stats{container_name} --no-stream {format}\");\n  let output = async_run_command(&command).await;\n  if output.success() {\n    output\n      .stdout\n      .split('\\n')\n      .filter(|e| !e.is_empty())\n      .map(|e| {\n        let parsed = serde_json::from_str(e)\n          .context(format!(\"failed at parsing entry {e}\"))?;\n        Ok(parsed)\n      })\n      .collect()\n  } else {\n    Err(anyhow!(\"{}\", output.stderr.replace('\\n', \" | \")))\n  }\n}\n\nimpl DockerClient {\n  /// Calls for stats once, similar to --no-stream on the cli\n  pub async fn full_container_stats(\n    &self,\n    container_name: &str,\n  ) -> anyhow::Result<FullContainerStats> {\n    let mut res = self.docker.stats(\n      container_name,\n      StatsOptionsBuilder::new().stream(false).build().into(),\n    );\n    let stats = res\n      .next()\n      .await\n      .with_context(|| format!(\"Unable to get container stats for {container_name} (got None)\"))?\n      .with_context(|| format!(\"Unable to get container stats for {container_name}\"))?;\n    Ok(FullContainerStats {\n      name: stats.name.unwrap_or(container_name.to_string()),\n      id: stats.id,\n      read: stats.read,\n      preread: stats.preread,\n      pids_stats: stats.pids_stats.map(convert_pids_stats),\n      blkio_stats: stats.blkio_stats.map(convert_blkio_stats),\n      num_procs: stats.num_procs,\n      storage_stats: stats.storage_stats.map(convert_storage_stats),\n      cpu_stats: stats.cpu_stats.map(convert_cpu_stats),\n      precpu_stats: stats.precpu_stats.map(convert_cpu_stats),\n      memory_stats: stats.memory_stats.map(convert_memory_stats),\n      networks: stats.networks.map(convert_network_stats),\n    })\n  }\n}\n\nfn convert_pids_stats(\n  pids_stats: models::ContainerPidsStats,\n) -> ContainerPidsStats {\n  ContainerPidsStats {\n    current: pids_stats.current,\n    limit: pids_stats.limit,\n  }\n}\n\nfn convert_blkio_stats(\n  blkio_stats: models::ContainerBlkioStats,\n) -> ContainerBlkioStats {\n  ContainerBlkioStats {\n    io_service_bytes_recursive: blkio_stats\n      .io_service_bytes_recursive\n      .map(convert_blkio_stat_entries),\n    io_serviced_recursive: blkio_stats\n      .io_serviced_recursive\n      .map(convert_blkio_stat_entries),\n    io_queue_recursive: blkio_stats\n      .io_queue_recursive\n      .map(convert_blkio_stat_entries),\n    io_service_time_recursive: blkio_stats\n      .io_service_time_recursive\n      .map(convert_blkio_stat_entries),\n    io_wait_time_recursive: blkio_stats\n      .io_wait_time_recursive\n      .map(convert_blkio_stat_entries),\n    io_merged_recursive: blkio_stats\n      .io_merged_recursive\n      .map(convert_blkio_stat_entries),\n    io_time_recursive: blkio_stats\n      .io_time_recursive\n      .map(convert_blkio_stat_entries),\n    sectors_recursive: blkio_stats\n      .sectors_recursive\n      .map(convert_blkio_stat_entries),\n  }\n}\n\nfn convert_blkio_stat_entries(\n  blkio_stat_entries: Vec<models::ContainerBlkioStatEntry>,\n) -> Vec<ContainerBlkioStatEntry> {\n  blkio_stat_entries\n    .into_iter()\n    .map(|blkio_stat_entry| ContainerBlkioStatEntry {\n      major: blkio_stat_entry.major,\n      minor: blkio_stat_entry.minor,\n      op: blkio_stat_entry.op,\n      value: blkio_stat_entry.value,\n    })\n    .collect()\n}\n\nfn convert_storage_stats(\n  storage_stats: models::ContainerStorageStats,\n) -> ContainerStorageStats {\n  ContainerStorageStats {\n    read_count_normalized: storage_stats.read_count_normalized,\n    read_size_bytes: storage_stats.read_size_bytes,\n    write_count_normalized: storage_stats.write_count_normalized,\n    write_size_bytes: storage_stats.write_size_bytes,\n  }\n}\n\nfn convert_cpu_stats(\n  cpu_stats: models::ContainerCpuStats,\n) -> ContainerCpuStats {\n  ContainerCpuStats {\n    cpu_usage: cpu_stats.cpu_usage.map(convert_cpu_usage),\n    system_cpu_usage: cpu_stats.system_cpu_usage,\n    online_cpus: cpu_stats.online_cpus,\n    throttling_data: cpu_stats\n      .throttling_data\n      .map(convert_cpu_throttling_data),\n  }\n}\n\nfn convert_cpu_usage(\n  cpu_usage: models::ContainerCpuUsage,\n) -> ContainerCpuUsage {\n  ContainerCpuUsage {\n    total_usage: cpu_usage.total_usage,\n    percpu_usage: cpu_usage.percpu_usage,\n    usage_in_kernelmode: cpu_usage.usage_in_kernelmode,\n    usage_in_usermode: cpu_usage.usage_in_usermode,\n  }\n}\n\nfn convert_cpu_throttling_data(\n  cpu_throttling_data: models::ContainerThrottlingData,\n) -> ContainerThrottlingData {\n  ContainerThrottlingData {\n    periods: cpu_throttling_data.periods,\n    throttled_periods: cpu_throttling_data.throttled_periods,\n    throttled_time: cpu_throttling_data.throttled_time,\n  }\n}\n\nfn convert_memory_stats(\n  memory_stats: models::ContainerMemoryStats,\n) -> ContainerMemoryStats {\n  ContainerMemoryStats {\n    usage: memory_stats.usage,\n    max_usage: memory_stats.max_usage,\n    stats: memory_stats.stats,\n    failcnt: memory_stats.failcnt,\n    limit: memory_stats.limit,\n    commitbytes: memory_stats.commitbytes,\n    commitpeakbytes: memory_stats.commitpeakbytes,\n    privateworkingset: memory_stats.privateworkingset,\n  }\n}\n\nfn convert_network_stats(\n  network_stats: HashMap<\n    std::string::String,\n    models::ContainerNetworkStats,\n  >,\n) -> HashMap<std::string::String, ContainerNetworkStats> {\n  network_stats\n    .into_iter()\n    .map(|(name, network_stats)| {\n      (\n        name,\n        ContainerNetworkStats {\n          rx_bytes: network_stats.rx_bytes,\n          rx_packets: network_stats.rx_packets,\n          rx_errors: network_stats.rx_errors,\n          rx_dropped: network_stats.rx_dropped,\n          tx_bytes: network_stats.tx_bytes,\n          tx_packets: network_stats.tx_packets,\n          tx_errors: network_stats.tx_errors,\n          tx_dropped: network_stats.tx_dropped,\n          endpoint_id: network_stats.endpoint_id,\n          instance_id: network_stats.instance_id,\n        },\n      )\n    })\n    .collect()\n}\n"
  },
  {
    "path": "bin/periphery/src/docker/volumes.rs",
    "content": "use bollard::query_parameters::ListVolumesOptions;\nuse komodo_client::entities::docker::{\n  PortBinding, container::ContainerListItem, volume::*,\n};\n\nuse crate::docker::DockerClient;\n\nimpl DockerClient {\n  pub async fn list_volumes(\n    &self,\n    containers: &[ContainerListItem],\n  ) -> anyhow::Result<Vec<VolumeListItem>> {\n    let volumes = self\n      .docker\n      .list_volumes(Option::<ListVolumesOptions>::None)\n      .await?\n      .volumes\n      .unwrap_or_default()\n      .into_iter()\n      .map(|volume| {\n        let scope = volume\n          .scope\n          .map(|scope| match scope {\n            bollard::secret::VolumeScopeEnum::EMPTY => {\n              VolumeScopeEnum::Empty\n            }\n            bollard::secret::VolumeScopeEnum::LOCAL => {\n              VolumeScopeEnum::Local\n            }\n            bollard::secret::VolumeScopeEnum::GLOBAL => {\n              VolumeScopeEnum::Global\n            }\n          })\n          .unwrap_or(VolumeScopeEnum::Empty);\n        let in_use = containers.iter().any(|container| {\n          container.volumes.iter().any(|name| &volume.name == name)\n        });\n        VolumeListItem {\n          name: volume.name,\n          driver: volume.driver,\n          mountpoint: volume.mountpoint,\n          created: volume.created_at,\n          size: volume.usage_data.map(|data| data.size),\n          scope,\n          in_use,\n        }\n      })\n      .collect();\n    Ok(volumes)\n  }\n\n  pub async fn inspect_volume(\n    &self,\n    volume_name: &str,\n  ) -> anyhow::Result<Volume> {\n    let volume = self.docker.inspect_volume(volume_name).await?;\n    Ok(Volume {\n      name: volume.name,\n      driver: volume.driver,\n      mountpoint: volume.mountpoint,\n      created_at: volume.created_at,\n      status: volume.status.unwrap_or_default().into_keys().map(|k| (k, Default::default())).collect(),\n      labels: volume.labels,\n      scope: volume\n        .scope\n        .map(|scope| match scope {\n          bollard::secret::VolumeScopeEnum::EMPTY => {\n            VolumeScopeEnum::Empty\n          }\n          bollard::secret::VolumeScopeEnum::LOCAL => {\n            VolumeScopeEnum::Local\n          }\n          bollard::secret::VolumeScopeEnum::GLOBAL => {\n            VolumeScopeEnum::Global\n          }\n        })\n        .unwrap_or_default(),\n      cluster_volume: volume.cluster_volume.map(|volume| {\n        ClusterVolume {\n          id: volume.id,\n          version: volume.version.map(|version| ObjectVersion {\n            index: version.index,\n          }),\n          created_at: volume.created_at,\n          updated_at: volume.updated_at,\n          spec: volume.spec.map(|spec| ClusterVolumeSpec {\n            group: spec.group,\n            access_mode: spec.access_mode.map(|mode| {\n              ClusterVolumeSpecAccessMode {\n                scope: mode.scope.map(|scope| match scope {\n                  bollard::secret::ClusterVolumeSpecAccessModeScopeEnum::EMPTY => ClusterVolumeSpecAccessModeScopeEnum::Empty,\n                  bollard::secret::ClusterVolumeSpecAccessModeScopeEnum::SINGLE => ClusterVolumeSpecAccessModeScopeEnum::Single,\n                  bollard::secret::ClusterVolumeSpecAccessModeScopeEnum::MULTI => ClusterVolumeSpecAccessModeScopeEnum::Multi,\n                }).unwrap_or_default(),\n                sharing: mode.sharing.map(|sharing| match sharing {\n                  bollard::secret::ClusterVolumeSpecAccessModeSharingEnum::EMPTY => ClusterVolumeSpecAccessModeSharingEnum::Empty,\n                  bollard::secret::ClusterVolumeSpecAccessModeSharingEnum::NONE => ClusterVolumeSpecAccessModeSharingEnum::None,\n                  bollard::secret::ClusterVolumeSpecAccessModeSharingEnum::READONLY => ClusterVolumeSpecAccessModeSharingEnum::Readonly,\n                  bollard::secret::ClusterVolumeSpecAccessModeSharingEnum::ONEWRITER => ClusterVolumeSpecAccessModeSharingEnum::Onewriter,\n                  bollard::secret::ClusterVolumeSpecAccessModeSharingEnum::ALL => ClusterVolumeSpecAccessModeSharingEnum::All,\n                }).unwrap_or_default(),\n                secrets: mode.secrets.unwrap_or_default().into_iter().map(|secret| ClusterVolumeSpecAccessModeSecrets {\n                    key: secret.key,\n                    secret: secret.secret,\n                }).collect(),\n                accessibility_requirements: mode\n                  .accessibility_requirements.map(|req| ClusterVolumeSpecAccessModeAccessibilityRequirements {\n                    requisite: req.requisite.unwrap_or_default().into_iter().map(|map| map.into_iter().map(|(k, v)| (k, v.unwrap_or_default().into_iter().map(|p| PortBinding { host_ip: p.host_ip, host_port: p.host_port }).collect())).collect()).collect(),\n                    preferred: req.preferred.unwrap_or_default().into_iter().map(|map| map.into_iter().map(|(k, v)| (k, v.unwrap_or_default().into_iter().map(|p| PortBinding { host_ip: p.host_ip, host_port: p.host_port }).collect())).collect()).collect(),\n                }),\n                capacity_range: mode.capacity_range.map(|range| ClusterVolumeSpecAccessModeCapacityRange {\n                  required_bytes: range.required_bytes,\n                  limit_bytes: range.limit_bytes,\n                }),\n                availability: mode.availability.map(|availability| match availability {\n                  bollard::secret::ClusterVolumeSpecAccessModeAvailabilityEnum::EMPTY => ClusterVolumeSpecAccessModeAvailabilityEnum::Empty,\n                  bollard::secret::ClusterVolumeSpecAccessModeAvailabilityEnum::ACTIVE => ClusterVolumeSpecAccessModeAvailabilityEnum::Active,\n                  bollard::secret::ClusterVolumeSpecAccessModeAvailabilityEnum::PAUSE => ClusterVolumeSpecAccessModeAvailabilityEnum::Pause,\n                  bollard::secret::ClusterVolumeSpecAccessModeAvailabilityEnum::DRAIN => ClusterVolumeSpecAccessModeAvailabilityEnum::Drain,\n                }).unwrap_or_default(),\n              }\n            }),\n          }),\n          info: volume.info.map(|info| ClusterVolumeInfo {\n            capacity_bytes: info.capacity_bytes,\n            volume_context: info.volume_context.unwrap_or_default(),\n            volume_id: info.volume_id,\n            accessible_topology: info.accessible_topology.unwrap_or_default().into_iter().map(|map| map.into_iter().map(|(k, v)| (k, v.unwrap_or_default().into_iter().map(|p| PortBinding { host_ip: p.host_ip, host_port: p.host_port }).collect())).collect()).collect(),\n          }),\n          publish_status: volume\n            .publish_status\n            .unwrap_or_default()\n            .into_iter()\n            .map(|status| ClusterVolumePublishStatus {\n              node_id: status.node_id,\n              state: status.state.map(|state| match state {\n                bollard::secret::ClusterVolumePublishStatusStateEnum::EMPTY => ClusterVolumePublishStatusStateEnum::Empty,\n                bollard::secret::ClusterVolumePublishStatusStateEnum::PENDING_PUBLISH => ClusterVolumePublishStatusStateEnum::PendingPublish,\n                bollard::secret::ClusterVolumePublishStatusStateEnum::PUBLISHED => ClusterVolumePublishStatusStateEnum::Published,\n                bollard::secret::ClusterVolumePublishStatusStateEnum::PENDING_NODE_UNPUBLISH => ClusterVolumePublishStatusStateEnum::PendingNodeUnpublish,\n                bollard::secret::ClusterVolumePublishStatusStateEnum::PENDING_CONTROLLER_UNPUBLISH => ClusterVolumePublishStatusStateEnum::PendingControllerUnpublish,\n              }).unwrap_or_default(),\n              publish_context: status.publish_context.unwrap_or_default(),\n            })\n            .collect(),\n        }\n      }),\n      options: volume.options,\n      usage_data: volume.usage_data.map(|data| VolumeUsageData {\n        size: data.size,\n        ref_count: data.ref_count,\n      }),\n    })\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/git.rs",
    "content": "use std::path::PathBuf;\n\nuse command::run_komodo_command_with_sanitization;\nuse environment::write_env_file;\nuse interpolate::Interpolator;\nuse komodo_client::entities::{\n  EnvironmentVar, RepoExecutionResponse, SystemCommand,\n  all_logs_success,\n};\nuse periphery_client::api::git::PeripheryRepoExecutionResponse;\n\nuse crate::config::periphery_config;\n\npub async fn handle_post_repo_execution(\n  mut res: RepoExecutionResponse,\n  mut environment: Vec<EnvironmentVar>,\n  env_file_path: &str,\n  mut on_clone: Option<SystemCommand>,\n  mut on_pull: Option<SystemCommand>,\n  skip_secret_interp: bool,\n  mut replacers: Vec<(String, String)>,\n) -> anyhow::Result<PeripheryRepoExecutionResponse> {\n  if !skip_secret_interp {\n    let mut interpolotor =\n      Interpolator::new(None, &periphery_config().secrets);\n    interpolotor.interpolate_env_vars(&mut environment)?;\n    if let Some(on_clone) = on_clone.as_mut() {\n      interpolotor.interpolate_string(&mut on_clone.command)?;\n    }\n    if let Some(on_pull) = on_pull.as_mut() {\n      interpolotor.interpolate_string(&mut on_pull.command)?;\n    }\n    replacers.extend(interpolotor.secret_replacers);\n  }\n\n  let env_file_path = write_env_file(\n    &environment,\n    &res.path,\n    env_file_path,\n    &mut res.logs,\n  )\n  .await;\n\n  let mut res = PeripheryRepoExecutionResponse { res, env_file_path };\n\n  if let Some(on_clone) = on_clone\n    && !on_clone.is_none()\n  {\n    let path = res\n      .res\n      .path\n      .join(on_clone.path)\n      .components()\n      .collect::<PathBuf>();\n    if let Some(log) = run_komodo_command_with_sanitization(\n      \"On Clone\",\n      path.as_path(),\n      on_clone.command,\n      true,\n      &replacers,\n    )\n    .await\n    {\n      res.res.logs.push(log);\n      if !all_logs_success(&res.res.logs) {\n        return Ok(res);\n      }\n    }\n  }\n\n  if let Some(on_pull) = on_pull\n    && !on_pull.is_none()\n  {\n    let path = res\n      .res\n      .path\n      .join(on_pull.path)\n      .components()\n      .collect::<PathBuf>();\n    if let Some(log) = run_komodo_command_with_sanitization(\n      \"On Pull\",\n      path.as_path(),\n      on_pull.command,\n      true,\n      &replacers,\n    )\n    .await\n    {\n      res.res.logs.push(log);\n    }\n  }\n\n  Ok(res)\n}\n"
  },
  {
    "path": "bin/periphery/src/helpers.rs",
    "content": "use anyhow::Context;\nuse komodo_client::{\n  entities::{EnvironmentVar, RepoExecutionArgs, SearchCombinator},\n  parsers::QUOTE_PATTERN,\n};\n\nuse crate::config::periphery_config;\n\npub fn git_token_simple(\n  domain: &str,\n  account_username: &str,\n) -> anyhow::Result<&'static str> {\n  periphery_config()\n    .git_providers\n    .iter()\n    .find(|provider| provider.domain == domain)\n    .and_then(|provider| {\n      provider.accounts.iter().find(|account| account.username == account_username).map(|account| account.token.as_str())\n    })\n    .with_context(|| format!(\"Did not find token in config for git account {account_username} | domain {domain}\"))\n}\n\npub fn git_token(\n  core_token: Option<String>,\n  args: &RepoExecutionArgs,\n) -> anyhow::Result<Option<String>> {\n  if core_token.is_some() {\n    return Ok(core_token);\n  }\n  let Some(account) = &args.account else {\n    return Ok(None);\n  };\n  let token = git_token_simple(&args.provider, account)?;\n  Ok(Some(token.to_string()))\n}\n\npub fn registry_token(\n  domain: &str,\n  account_username: &str,\n) -> anyhow::Result<&'static str> {\n  periphery_config()\n    .docker_registries\n    .iter()\n    .find(|registry| registry.domain == domain)\n    .and_then(|registry| {\n      registry.accounts.iter().find(|account| account.username == account_username).map(|account| account.token.as_str())\n    })\n    .with_context(|| format!(\"did not find token in config for docker registry account {account_username} | domain {domain}\"))\n}\n\npub fn parse_extra_args(extra_args: &[String]) -> String {\n  let args = extra_args.join(\" \");\n  if !args.is_empty() {\n    format!(\" {args}\")\n  } else {\n    args\n  }\n}\n\npub fn parse_labels(labels: &[EnvironmentVar]) -> String {\n  labels\n    .iter()\n    .map(|p| {\n      if p.value.starts_with(QUOTE_PATTERN)\n        && p.value.ends_with(QUOTE_PATTERN)\n      {\n        // If the value already wrapped in quotes, don't wrap it again\n        format!(\" --label {}={}\", p.variable, p.value)\n      } else {\n        format!(\" --label {}=\\\"{}\\\"\", p.variable, p.value)\n      }\n    })\n    .collect::<Vec<_>>()\n    .join(\"\")\n}\n\npub fn log_grep(\n  terms: &[String],\n  combinator: SearchCombinator,\n  invert: bool,\n) -> String {\n  let maybe_invert = if invert { \" -v\" } else { Default::default() };\n  match combinator {\n    SearchCombinator::Or => {\n      format!(\"grep{maybe_invert} -E '{}'\", terms.join(\"|\"))\n    }\n    SearchCombinator::And => {\n      format!(\n        \"grep{maybe_invert} -P '^(?=.*{})'\",\n        terms.join(\")(?=.*\")\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/main.rs",
    "content": "#[macro_use]\nextern crate tracing;\n\n//\nuse std::{net::SocketAddr, str::FromStr};\n\nuse anyhow::Context;\nuse axum_server::tls_rustls::RustlsConfig;\nuse config::periphery_config;\n\nmod api;\nmod build;\nmod compose;\nmod config;\nmod docker;\nmod git;\nmod helpers;\nmod ssl;\nmod stats;\nmod terminal;\n\nasync fn app() -> anyhow::Result<()> {\n  dotenvy::dotenv().ok();\n  let config = config::periphery_config();\n  logger::init(&config.logging)?;\n\n  info!(\"Komodo Periphery version: v{}\", env!(\"CARGO_PKG_VERSION\"));\n\n  if periphery_config().pretty_startup_config {\n    info!(\"{:#?}\", config.sanitized());\n  } else {\n    info!(\"{:?}\", config.sanitized());\n  }\n\n  stats::spawn_polling_thread();\n  docker::stats::spawn_polling_thread();\n\n  let addr = format!(\n    \"{}:{}\",\n    config::periphery_config().bind_ip,\n    config::periphery_config().port\n  );\n\n  let socket_addr = SocketAddr::from_str(&addr)\n    .context(\"failed to parse listen address\")?;\n\n  let app =\n    api::router().into_make_service_with_connect_info::<SocketAddr>();\n\n  if config.ssl_enabled {\n    info!(\"🔒 Periphery SSL Enabled\");\n    rustls::crypto::ring::default_provider()\n      .install_default()\n      .expect(\"failed to install default rustls CryptoProvider\");\n    ssl::ensure_certs().await;\n    info!(\"Komodo Periphery starting on https://{}\", socket_addr);\n    let ssl_config = RustlsConfig::from_pem_file(\n      config.ssl_cert_file(),\n      config.ssl_key_file(),\n    )\n    .await\n    .context(\"Invalid ssl cert / key\")?;\n    axum_server::bind_rustls(socket_addr, ssl_config)\n      .serve(app)\n      .await?\n  } else {\n    info!(\"🔓 Periphery SSL Disabled\");\n    info!(\"Komodo Periphery starting on http://{}\", socket_addr);\n    axum_server::bind(socket_addr).serve(app).await?\n  }\n\n  Ok(())\n}\n\n#[tokio::main]\nasync fn main() -> anyhow::Result<()> {\n  let mut term_signal = tokio::signal::unix::signal(\n    tokio::signal::unix::SignalKind::terminate(),\n  )?;\n\n  let app = tokio::spawn(app());\n\n  tokio::select! {\n    res = app => return res?,\n    _ = term_signal.recv() => {\n      info!(\"Exiting all active Terminals for shutdown\");\n      terminal::delete_all_terminals().await;\n    },\n  }\n\n  Ok(())\n}\n"
  },
  {
    "path": "bin/periphery/src/ssl.rs",
    "content": "use crate::config::periphery_config;\n\npub async fn ensure_certs() {\n  let config = periphery_config();\n  if !config.ssl_cert_file().is_file()\n    || !config.ssl_key_file().is_file()\n  {\n    generate_self_signed_ssl_certs().await\n  }\n}\n\n#[instrument]\nasync fn generate_self_signed_ssl_certs() {\n  info!(\"Generating certs...\");\n\n  let config = periphery_config();\n\n  let ssl_key_file = config.ssl_key_file();\n  let ssl_cert_file = config.ssl_cert_file();\n\n  // ensure cert folders exist\n  if let Some(parent) = ssl_key_file.parent() {\n    let _ = std::fs::create_dir_all(parent);\n  }\n  if let Some(parent) = ssl_cert_file.parent() {\n    let _ = std::fs::create_dir_all(parent);\n  }\n\n  let key_path = ssl_key_file.display();\n  let cert_path = ssl_cert_file.display();\n\n  let command = format!(\n    \"openssl req -x509 -newkey rsa:4096 -keyout {key_path} -out {cert_path} -sha256 -days 3650 -nodes -subj \\\"/C=XX/CN=periphery\\\"\"\n  );\n  let log = run_command::async_run_command(&command).await;\n\n  if log.success() {\n    info!(\"✅ SSL Certs generated\");\n  } else {\n    panic!(\n      \"🚨 Failed to generate SSL Certs | stdout: {} | stderr: {}\",\n      log.stdout, log.stderr\n    );\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/stats.rs",
    "content": "use std::{cmp::Ordering, sync::OnceLock};\n\nuse async_timing_util::wait_until_timelength;\nuse komodo_client::entities::stats::{\n  SingleDiskUsage, SystemInformation, SystemLoadAverage,\n  SystemProcess, SystemStats,\n};\nuse sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};\nuse tokio::sync::RwLock;\n\nuse crate::config::periphery_config;\n\npub fn stats_client() -> &'static RwLock<StatsClient> {\n  static STATS_CLIENT: OnceLock<RwLock<StatsClient>> =\n    OnceLock::new();\n  STATS_CLIENT.get_or_init(|| RwLock::new(StatsClient::default()))\n}\n\n/// This should be called before starting the server in main.rs.\n/// Keeps the cached stats up to date\npub fn spawn_polling_thread() {\n  tokio::spawn(async move {\n    let polling_rate = periphery_config()\n      .stats_polling_rate\n      .to_string()\n      .parse()\n      .expect(\"invalid stats polling rate\");\n    let client = stats_client();\n    loop {\n      let ts = wait_until_timelength(polling_rate, 1).await;\n      let mut client = client.write().await;\n      client.refresh();\n      client.stats = client.get_system_stats();\n      client.stats.refresh_ts = ts as i64;\n    }\n  });\n}\n\npub struct StatsClient {\n  /// Cached system stats\n  pub stats: SystemStats,\n  /// Cached system information\n  pub info: SystemInformation,\n\n  // the handles used to get the stats\n  system: sysinfo::System,\n  disks: sysinfo::Disks,\n  networks: sysinfo::Networks,\n}\n\nconst BYTES_PER_GB: f64 = 1073741824.0;\nconst BYTES_PER_MB: f64 = 1048576.0;\nconst BYTES_PER_KB: f64 = 1024.0;\n\nimpl Default for StatsClient {\n  fn default() -> Self {\n    let system = sysinfo::System::new_all();\n    let disks = sysinfo::Disks::new_with_refreshed_list();\n    let networks = sysinfo::Networks::new_with_refreshed_list();\n    let stats = SystemStats {\n      polling_rate: periphery_config().stats_polling_rate,\n      ..Default::default()\n    };\n    StatsClient {\n      info: get_system_information(&system),\n      system,\n      disks,\n      networks,\n      stats,\n    }\n  }\n}\n\nimpl StatsClient {\n  fn refresh(&mut self) {\n    self.system.refresh_cpu_all();\n    self.system.refresh_memory();\n    self.system.refresh_processes_specifics(\n      ProcessesToUpdate::All,\n      true,\n      ProcessRefreshKind::everything().without_tasks(),\n    );\n    self.disks.refresh(true);\n    self.networks.refresh(true);\n  }\n\n  pub fn get_system_stats(&self) -> SystemStats {\n    let total_mem = self.system.total_memory();\n    let available_mem = self.system.available_memory();\n\n    let mut network_ingress_bytes: u64 = 0;\n    let mut network_egress_bytes: u64 = 0;\n\n    for (_, network) in self.networks.iter() {\n      network_ingress_bytes += network.received();\n      network_egress_bytes += network.transmitted();\n    }\n\n    let load_avg = System::load_average();\n\n    SystemStats {\n      cpu_perc: self.system.global_cpu_usage(),\n      load_average: SystemLoadAverage {\n        one: load_avg.one,\n        five: load_avg.five,\n        fifteen: load_avg.fifteen,\n      },\n      mem_free_gb: self.system.free_memory() as f64 / BYTES_PER_GB,\n      mem_used_gb: (total_mem - available_mem) as f64 / BYTES_PER_GB,\n      mem_total_gb: total_mem as f64 / BYTES_PER_GB,\n      network_ingress_bytes: network_ingress_bytes as f64,\n      network_egress_bytes: network_egress_bytes as f64,\n      disks: self.get_disks(),\n      polling_rate: self.stats.polling_rate,\n      refresh_ts: self.stats.refresh_ts,\n      refresh_list_ts: self.stats.refresh_list_ts,\n    }\n  }\n\n  fn get_disks(&self) -> Vec<SingleDiskUsage> {\n    let config = periphery_config();\n    self\n      .disks\n      .list()\n      .iter()\n      .filter(|d| {\n        if d.file_system() == \"overlay\" {\n          return false;\n        }\n        let path = d.mount_point();\n        for mount in config.exclude_disk_mounts.iter() {\n          if path == mount {\n            return false;\n          }\n        }\n        if config.include_disk_mounts.is_empty() {\n          return true;\n        }\n        for mount in config.include_disk_mounts.iter() {\n          if path == mount {\n            return true;\n          }\n        }\n        false\n      })\n      .map(|disk| {\n        let file_system =\n          disk.file_system().to_string_lossy().to_string();\n        let disk_total = disk.total_space() as f64 / BYTES_PER_GB;\n        let disk_free = disk.available_space() as f64 / BYTES_PER_GB;\n        SingleDiskUsage {\n          mount: disk.mount_point().to_owned(),\n          used_gb: disk_total - disk_free,\n          total_gb: disk_total,\n          file_system,\n        }\n      })\n      .collect()\n  }\n\n  pub fn get_processes(&self) -> Vec<SystemProcess> {\n    let mut procs: Vec<_> = self\n      .system\n      .processes()\n      .iter()\n      .map(|(pid, p)| {\n        let disk_usage = p.disk_usage();\n        SystemProcess {\n          pid: pid.as_u32(),\n          name: p.name().to_string_lossy().to_string(),\n          exe: p\n            .exe()\n            .map(|exe| exe.to_str().unwrap_or_default())\n            .unwrap_or_default()\n            .to_string(),\n          cmd: p\n            .cmd()\n            .iter()\n            .map(|cmd| cmd.to_string_lossy().to_string())\n            .collect(),\n          start_time: (p.start_time() * 1000) as f64,\n          cpu_perc: p.cpu_usage(),\n          mem_mb: p.memory() as f64 / BYTES_PER_MB,\n          disk_read_kb: disk_usage.read_bytes as f64 / BYTES_PER_KB,\n          disk_write_kb: disk_usage.written_bytes as f64\n            / BYTES_PER_KB,\n        }\n      })\n      .collect();\n    procs.sort_by(|a, b| {\n      if a.cpu_perc > b.cpu_perc {\n        Ordering::Less\n      } else {\n        Ordering::Greater\n      }\n    });\n    procs\n  }\n}\n\nfn get_system_information(\n  sys: &sysinfo::System,\n) -> SystemInformation {\n  let config = periphery_config();\n  SystemInformation {\n    name: System::name(),\n    os: System::long_os_version(),\n    kernel: System::kernel_version(),\n    host_name: System::host_name(),\n    core_count: System::physical_core_count().map(|c| c as u32),\n    cpu_brand: sys\n      .cpus()\n      .iter()\n      .next()\n      .map(|cpu| cpu.brand().to_string())\n      .unwrap_or_default(),\n    terminals_disabled: config.disable_terminals,\n    container_exec_disabled: config.disable_container_exec,\n  }\n}\n"
  },
  {
    "path": "bin/periphery/src/terminal.rs",
    "content": "use std::{\n  collections::{HashMap, VecDeque},\n  pin::Pin,\n  sync::{Arc, OnceLock},\n  task::Poll,\n  time::Duration,\n};\n\nuse anyhow::{Context, anyhow};\nuse axum::http::StatusCode;\nuse bytes::Bytes;\nuse futures::Stream;\nuse komodo_client::{\n  api::write::TerminalRecreateMode,\n  entities::{komodo_timestamp, server::TerminalInfo},\n};\nuse pin_project_lite::pin_project;\nuse portable_pty::{CommandBuilder, PtySize, native_pty_system};\nuse rand::Rng;\nuse serror::AddStatusCodeError;\nuse tokio::sync::{broadcast, mpsc};\nuse tokio_util::sync::CancellationToken;\n\ntype PtyName = String;\ntype PtyMap = tokio::sync::RwLock<HashMap<PtyName, Arc<Terminal>>>;\ntype StdinSender = mpsc::Sender<StdinMsg>;\ntype StdoutReceiver = broadcast::Receiver<Bytes>;\n\npub async fn create_terminal(\n  name: String,\n  command: String,\n  recreate: TerminalRecreateMode,\n) -> anyhow::Result<()> {\n  trace!(\n    \"CreateTerminal: {name} | command: {command} | recreate: {recreate:?}\"\n  );\n  let mut terminals = terminals().write().await;\n  use TerminalRecreateMode::*;\n  if matches!(recreate, Never | DifferentCommand)\n    && let Some(terminal) = terminals.get(&name)\n  {\n    if terminal.command == command {\n      return Ok(());\n    } else if matches!(recreate, Never) {\n      return Err(anyhow!(\n        \"Terminal {name} already exists, but has command {} instead of {command}\",\n        terminal.command\n      ));\n    }\n  }\n  if let Some(prev) = terminals.insert(\n    name,\n    Terminal::new(command)\n      .await\n      .context(\"Failed to init terminal\")?\n      .into(),\n  ) {\n    prev.cancel();\n  }\n  Ok(())\n}\n\npub async fn delete_terminal(name: &str) {\n  if let Some(terminal) = terminals().write().await.remove(name) {\n    terminal.cancel.cancel();\n  }\n}\n\npub async fn list_terminals() -> Vec<TerminalInfo> {\n  let mut terminals = terminals()\n    .read()\n    .await\n    .iter()\n    .map(|(name, terminal)| TerminalInfo {\n      name: name.to_string(),\n      command: terminal.command.clone(),\n      stored_size_kb: terminal.history.size_kb(),\n    })\n    .collect::<Vec<_>>();\n  terminals.sort_by(|a, b| a.name.cmp(&b.name));\n  terminals\n}\n\npub async fn get_terminal(\n  name: &str,\n) -> anyhow::Result<Arc<Terminal>> {\n  terminals()\n    .read()\n    .await\n    .get(name)\n    .cloned()\n    .with_context(|| format!(\"No terminal at {name}\"))\n}\n\npub async fn clean_up_terminals() {\n  terminals()\n    .write()\n    .await\n    .retain(|_, terminal| !terminal.cancel.is_cancelled());\n}\n\npub async fn delete_all_terminals() {\n  terminals()\n    .write()\n    .await\n    .drain()\n    .for_each(|(_, terminal)| terminal.cancel());\n  // The terminals poll cancel every 500 millis, need to wait for them\n  // to finish cancelling.\n  tokio::time::sleep(Duration::from_millis(100)).await;\n}\n\nfn terminals() -> &'static PtyMap {\n  static TERMINALS: OnceLock<PtyMap> = OnceLock::new();\n  TERMINALS.get_or_init(Default::default)\n}\n\n#[derive(Clone, serde::Deserialize)]\npub struct ResizeDimensions {\n  rows: u16,\n  cols: u16,\n}\n\n#[derive(Clone)]\npub enum StdinMsg {\n  Bytes(Bytes),\n  Resize(ResizeDimensions),\n}\n\npub struct Terminal {\n  /// The command that was used as the root command, eg `shell`\n  command: String,\n\n  pub cancel: CancellationToken,\n\n  pub stdin: StdinSender,\n  pub stdout: StdoutReceiver,\n\n  pub history: Arc<History>,\n}\n\nimpl Terminal {\n  async fn new(command: String) -> anyhow::Result<Terminal> {\n    trace!(\"Creating terminal with command: {command}\");\n\n    let terminal = native_pty_system()\n      .openpty(PtySize::default())\n      .context(\"Failed to open terminal\")?;\n\n    let mut command_split = command.split(' ').map(|arg| arg.trim());\n    let cmd =\n      command_split.next().context(\"Command cannot be empty\")?;\n\n    let mut cmd = CommandBuilder::new(cmd);\n\n    for arg in command_split {\n      cmd.arg(arg);\n    }\n\n    cmd.env(\"TERM\", \"xterm-256color\");\n    cmd.env(\"COLORTERM\", \"truecolor\");\n\n    let mut child = terminal\n      .slave\n      .spawn_command(cmd)\n      .context(\"Failed to spawn child command\")?;\n\n    // Check the child didn't stop immediately (after a little wait) with error\n    tokio::time::sleep(Duration::from_millis(100)).await;\n    if let Some(status) = child\n      .try_wait()\n      .context(\"Failed to check child process exit status\")?\n    {\n      return Err(anyhow!(\n        \"Child process exited immediately with code {}\",\n        status.exit_code()\n      ));\n    }\n\n    let mut terminal_write = terminal\n      .master\n      .take_writer()\n      .context(\"Failed to take terminal writer\")?;\n    let mut terminal_read = terminal\n      .master\n      .try_clone_reader()\n      .context(\"Failed to clone terminal reader\")?;\n\n    let cancel = CancellationToken::new();\n\n    // CHILD WAIT TASK\n    let _cancel = cancel.clone();\n    tokio::task::spawn_blocking(move || {\n      loop {\n        if _cancel.is_cancelled() {\n          trace!(\"child wait handle cancelled from outside\");\n          if let Err(e) = child.kill() {\n            debug!(\"Failed to kill child | {e:?}\");\n          }\n          break;\n        }\n        match child.try_wait() {\n          Ok(Some(code)) => {\n            debug!(\"child exited with code {code}\");\n            _cancel.cancel();\n            break;\n          }\n          Ok(None) => {\n            std::thread::sleep(Duration::from_millis(500));\n          }\n          Err(e) => {\n            debug!(\"failed to wait for child | {e:?}\");\n            _cancel.cancel();\n            break;\n          }\n        }\n      }\n    });\n\n    // WS (channel) -> STDIN TASK\n    // Theres only one consumer here, so use mpsc\n    let (stdin, mut channel_read) =\n      tokio::sync::mpsc::channel::<StdinMsg>(8192);\n    let _cancel = cancel.clone();\n    tokio::task::spawn_blocking(move || {\n      loop {\n        if _cancel.is_cancelled() {\n          trace!(\"terminal write: cancelled from outside\");\n          break;\n        }\n        match channel_read.blocking_recv() {\n          Some(StdinMsg::Bytes(bytes)) => {\n            if let Err(e) = terminal_write.write_all(&bytes) {\n              debug!(\"Failed to write to PTY: {e:?}\");\n              _cancel.cancel();\n              break;\n            }\n          }\n          Some(StdinMsg::Resize(dimensions)) => {\n            if let Err(e) = terminal.master.resize(PtySize {\n              cols: dimensions.cols,\n              rows: dimensions.rows,\n              pixel_width: 0,\n              pixel_height: 0,\n            }) {\n              debug!(\"Failed to resize | {e:?}\");\n              _cancel.cancel();\n              break;\n            };\n          }\n          None => {\n            debug!(\"WS -> PTY channel read error: Disconnected\");\n            _cancel.cancel();\n            break;\n          }\n        }\n      }\n    });\n\n    let history = Arc::new(History::default());\n\n    // PTY -> WS (channel) TASK\n    // Uses broadcast to output to multiple client simultaneously\n    let (write, stdout) =\n      tokio::sync::broadcast::channel::<Bytes>(8192);\n    let _cancel = cancel.clone();\n    let _history = history.clone();\n    tokio::task::spawn_blocking(move || {\n      let mut buf = [0u8; 8192];\n      loop {\n        if _cancel.is_cancelled() {\n          trace!(\"terminal read: cancelled from outside\");\n          break;\n        }\n        match terminal_read.read(&mut buf) {\n          Ok(0) => {\n            // EOF\n            trace!(\"Got PTY read EOF\");\n            _cancel.cancel();\n            break;\n          }\n          Ok(n) => {\n            _history.push(&buf[..n]);\n            if let Err(e) =\n              write.send(Bytes::copy_from_slice(&buf[..n]))\n            {\n              debug!(\"PTY -> WS channel send error: {e:?}\");\n              _cancel.cancel();\n              break;\n            }\n          }\n          Err(e) => {\n            debug!(\"Failed to read for PTY: {e:?}\");\n            _cancel.cancel();\n            break;\n          }\n        }\n      }\n    });\n\n    trace!(\"terminal tasks spawned\");\n\n    Ok(Terminal {\n      command,\n      cancel,\n      stdin,\n      stdout,\n      history,\n    })\n  }\n\n  pub fn cancel(&self) {\n    trace!(\"Cancel called\");\n    self.cancel.cancel();\n  }\n}\n\n/// 1 MiB rolling max history size per terminal\nconst MAX_BYTES: usize = 1024 * 1024;\n\npub struct History {\n  buf: std::sync::RwLock<VecDeque<u8>>,\n}\n\nimpl Default for History {\n  fn default() -> Self {\n    History {\n      buf: VecDeque::with_capacity(MAX_BYTES).into(),\n    }\n  }\n}\n\nimpl History {\n  /// Push some bytes, evicting the oldest when full.\n  fn push(&self, bytes: &[u8]) {\n    let mut buf = self.buf.write().unwrap();\n    for byte in bytes {\n      if buf.len() == MAX_BYTES {\n        buf.pop_front();\n      }\n      buf.push_back(*byte);\n    }\n  }\n\n  pub fn bytes_parts(&self) -> (Bytes, Bytes) {\n    let buf = self.buf.read().unwrap();\n    let (a, b) = buf.as_slices();\n    (Bytes::copy_from_slice(a), Bytes::copy_from_slice(b))\n  }\n\n  pub fn size_kb(&self) -> f64 {\n    self.buf.read().unwrap().len() as f64 / 1024.0\n  }\n}\n\n/// Execute Sentinels\npub const START_OF_OUTPUT: &str = \"__KOMODO_START_OF_OUTPUT__\";\npub const END_OF_OUTPUT: &str = \"__KOMODO_END_OF_OUTPUT__\";\n\npin_project! {\n  pub struct TerminalStream<S> { #[pin] pub stdout: S }\n}\n\nimpl<S> Stream for TerminalStream<S>\nwhere\n  S:\n    Stream<Item = Result<String, tokio_util::codec::LinesCodecError>>,\n{\n  // Axum expects a stream of results\n  type Item = Result<String, String>;\n\n  fn poll_next(\n    self: Pin<&mut Self>,\n    cx: &mut std::task::Context<'_>,\n  ) -> Poll<Option<Self::Item>> {\n    let this = self.project();\n    match this.stdout.poll_next(cx) {\n      Poll::Ready(None) => {\n        // This is if a None comes in before END_OF_OUTPUT.\n        // This probably means the terminal has exited early,\n        // and needs to be cleaned up\n        tokio::spawn(async move { clean_up_terminals().await });\n        Poll::Ready(None)\n      }\n      Poll::Ready(Some(line)) => {\n        match line {\n          Ok(line) if line.as_str() == END_OF_OUTPUT => {\n            // Stop the stream on end sentinel\n            Poll::Ready(None)\n          }\n          Ok(line) => Poll::Ready(Some(Ok(line + \"\\n\"))),\n          Err(e) => Poll::Ready(Some(Err(format!(\"{e:?}\")))),\n        }\n      }\n      Poll::Pending => Poll::Pending,\n    }\n  }\n}\n\n/// Tokens valid for 3 seconds\nconst TOKEN_VALID_FOR_MS: i64 = 3_000;\n\npub fn auth_tokens() -> &'static AuthTokens {\n  static AUTH_TOKENS: OnceLock<AuthTokens> = OnceLock::new();\n  AUTH_TOKENS.get_or_init(Default::default)\n}\n\n#[derive(Default)]\npub struct AuthTokens {\n  map: std::sync::Mutex<HashMap<String, i64>>,\n}\n\nimpl AuthTokens {\n  pub fn create_auth_token(&self) -> String {\n    let mut lock = self.map.lock().unwrap();\n    // clear out any old tokens here (prevent unbounded growth)\n    let ts = komodo_timestamp();\n    lock.retain(|_, valid_until| *valid_until > ts);\n    let token: String = rand::rng()\n      .sample_iter(&rand::distr::Alphanumeric)\n      .take(30)\n      .map(char::from)\n      .collect();\n    lock.insert(token.clone(), ts + TOKEN_VALID_FOR_MS);\n    token\n  }\n\n  pub fn check_token(&self, token: String) -> serror::Result<()> {\n    let Some(valid_until) = self.map.lock().unwrap().remove(&token)\n    else {\n      return Err(\n        anyhow!(\"Terminal auth token not found\")\n          .status_code(StatusCode::UNAUTHORIZED),\n      );\n    };\n    if komodo_timestamp() <= valid_until {\n      Ok(())\n    } else {\n      Err(\n        anyhow!(\"Terminal token is expired\")\n          .status_code(StatusCode::UNAUTHORIZED),\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "bin/periphery/starship.toml",
    "content": "## This is used to customize the shell prompt in Periphery container for Terminals\n\n\"$schema\" = 'https://starship.rs/config-schema.json'\n\nadd_newline = true\n\nformat = \"$time$hostname$container$memory_usage$all\"\n\n[character]\nsuccess_symbol = \"[❯](bright-blue bold)\"\nerror_symbol = \"[❯](bright-red bold)\"\n\n[package]\ndisabled = true\n\n[time]\nformat = \"[❯$time](white dimmed) \"\ntime_format = \"%l:%M %p\"\nutc_time_offset = '-5'\ndisabled = true\n\n[username]\nformat = \"[❯ $user]($style) \"\nstyle_user = \"bright-green\"\nshow_always = true\n\n[hostname]\nformat = \"[❯ $hostname]($style) \"\nstyle = \"bright-blue\"\nssh_only = false\n\n[directory]\nformat = \"[❯ $path]($style)[$read_only]($read_only_style) \"\nstyle = \"bright-cyan\"\n\n[git_branch]\nformat = \"[❯ $symbol$branch(:$remote_branch)]($style) \"\nstyle = \"bright-purple\"\n\n[git_status]\nstyle = \"bright-purple\"\n\n[rust]\nformat = \"[❯ $symbol($version )]($style)\"\nsymbol = \"rustc \"\nstyle = \"bright-red\"\n\n[nodejs]\nformat = \"[❯ $symbol($version )]($style)\"\nsymbol = \"nodejs \"\nstyle = \"bright-red\"\n\n[memory_usage]\nformat = \"[❯ mem ${ram} ${ram_pct}]($style) \"\nthreshold = -1\nstyle = \"white\"\n\n[cmd_duration]\nformat = \"[❯ $duration]($style)\"\nstyle = \"bright-yellow\"\n\n[container]\nformat = \"[❯ 🦎 periphery container ]($style)\"\nstyle = \"bright-green\"\n\n[aws]\ndisabled = true\n"
  },
  {
    "path": "client/core/rs/Cargo.toml",
    "content": "[package]\nname = \"komodo_client\"\ndescription = \"Client for the Komodo build and deployment system\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[features]\n# default = [\"blocking\"] # use to dev client blocking mode\nmongo = [\"dep:mongo_indexed\"]\nblocking = [\"reqwest/blocking\"]\n\n[dependencies]\n# mogh\nmongo_indexed = { workspace = true, optional = true }\nserror = { workspace = true, features = [\"axum\"]}\nderive_default_builder.workspace = true\nderive_empty_traits.workspace = true\nasync_timing_util.workspace = true\npartial_derive2.workspace = true\nderive_variants.workspace = true\nresolver_api.workspace = true\n# external\ntokio-tungstenite.workspace = true\nderive_builder.workspace = true\nurlencoding.workspace = true\nserde_json.workspace = true\ntokio-util.workspace = true\nthiserror.workspace = true\ntypeshare.workspace = true\nindexmap.workspace = true\nserde_qs.workspace = true\nfutures.workspace = true\nreqwest.workspace = true\ntracing.workspace = true\nanyhow.workspace = true\nserde.workspace = true\ntokio.workspace = true\nstrum.workspace = true\nenvy.workspace = true\nuuid.workspace = true\nclap.workspace = true\nbson.workspace = true\nipnetwork.workspace = true"
  },
  {
    "path": "client/core/rs/README.md",
    "content": "# Komodo\n*A system to build and deploy software across many servers*. [https://komo.do](https://komo.do)\n\nDocs: [https://docs.rs/komodo_client/latest/komodo_client](https://docs.rs/komodo_client/latest/komodo_client).\n\nThis is a client library for the Komodo Core API.\nIt contains:\n- Definitions for the application [api](https://docs.rs/komodo_client/latest/komodo_client/api/index.html)\n\tand [entities](https://docs.rs/komodo_client/latest/komodo_client/entities/index.html).\n- A [client](https://docs.rs/komodo_client/latest/komodo_client/struct.KomodoClient.html)\n\tto interact with the Komodo Core API.\n- Information on configuring Komodo\n\t[Core](https://docs.rs/komodo_client/latest/komodo_client/entities/config/core/index.html) and\n\t[Periphery](https://docs.rs/komodo_client/latest/komodo_client/entities/config/periphery/index.html).\n\n## Client Configuration\n\nThe client includes a convenenience method to parse the Komodo API url and credentials from the environment:\n- `KOMODO_ADDRESS`\n- `KOMODO_API_KEY`\n- `KOMODO_API_SECRET`\n\n## Client Example\n```rust\ndotenvy::dotenv().ok();\n\nlet client = KomodoClient::new_from_env()?;\n\n// Get all the deployments\nlet deployments = client.read(ListDeployments::default()).await?;\n\nprintln!(\"{deployments:#?}\");\n\nlet update = client.execute(RunBuild { build: \"test-build\".to_string() }).await?:\n```"
  },
  {
    "path": "client/core/rs/src/api/auth.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::{HasResponse, Resolve};\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::user::User;\n\npub trait KomodoAuthRequest: HasResponse {}\n\n/// JSON containing an authentication token.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct JwtResponse {\n  /// User ID for signed in user.\n  pub user_id: String,\n  /// A token the user can use to authenticate their requests.\n  pub jwt: String,\n}\n\n//\n\n/// Non authenticated route to see the available options\n/// users have to login to Komodo, eg. local auth, github, google.\n/// Response: [GetLoginOptionsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoAuthRequest)]\n#[response(GetLoginOptionsResponse)]\n#[error(serror::Error)]\npub struct GetLoginOptions {}\n\n/// The response for [GetLoginOptions].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Copy)]\npub struct GetLoginOptionsResponse {\n  /// Whether local auth is enabled.\n  pub local: bool,\n  /// Whether github login is enabled.\n  pub github: bool,\n  /// Whether google login is enabled.\n  pub google: bool,\n  /// Whether OIDC login is enabled.\n  pub oidc: bool,\n  /// Whether user registration (Sign Up) has been disabled\n  pub registration_disabled: bool,\n}\n\n//\n\n/// Sign up a new local user account. Will fail if a user with the\n/// given username already exists.\n/// Response: [SignUpLocalUserResponse].\n///\n/// Note. This method is only available if the core api has `local_auth` enabled,\n/// and if user registration is not disabled (after the first user).\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoAuthRequest)]\n#[response(SignUpLocalUserResponse)]\n#[error(serror::Error)]\npub struct SignUpLocalUser {\n  /// The username for the new user.\n  pub username: String,\n  /// The password for the new user.\n  /// This cannot be retreived later.\n  pub password: String,\n}\n\n/// Response for [SignUpLocalUser].\n#[typeshare]\npub type SignUpLocalUserResponse = JwtResponse;\n\n//\n\n/// Login as a local user. Will fail if the users credentials don't match\n/// any local user.\n///\n/// Note. This method is only available if the core api has `local_auth` enabled.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoAuthRequest)]\n#[response(LoginLocalUserResponse)]\n#[error(serror::Error)]\npub struct LoginLocalUser {\n  /// The user's username\n  pub username: String,\n  /// The user's password\n  pub password: String,\n}\n\n/// The response for [LoginLocalUser]\n#[typeshare]\npub type LoginLocalUserResponse = JwtResponse;\n\n//\n\n/// Exchange a single use exchange token (safe for transport in url query)\n/// for a jwt.\n/// Response: [ExchangeForJwtResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoAuthRequest)]\n#[response(ExchangeForJwtResponse)]\n#[error(serror::Error)]\npub struct ExchangeForJwt {\n  /// The 'exchange token'\n  pub token: String,\n}\n\n/// Response for [ExchangeForJwt].\n#[typeshare]\npub type ExchangeForJwtResponse = JwtResponse;\n\n//\n\n/// Get the user extracted from the request headers.\n/// Response: [User].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoAuthRequest)]\n#[response(GetUserResponse)]\n#[error(serror::Error)]\npub struct GetUser {}\n\n#[typeshare]\npub type GetUserResponse = User;\n\n//\n"
  },
  {
    "path": "client/core/rs/src/api/execute/action.rs",
    "content": "use anyhow::Context;\nuse clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{JsonObject, update::Update};\n\nuse super::{BatchExecutionResponse, KomodoExecuteRequest};\n\n/// Runs the target Action. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RunAction {\n  /// Id or name\n  pub action: String,\n\n  /// Custom arguments which are merged on top of the default arguments.\n  /// CLI Format: `\"VAR1=val1&VAR2=val2\"`\n  ///\n  /// Webhook-triggered actions use this to pass WEBHOOK_BRANCH and WEBHOOK_BODY.\n  #[clap(value_parser = args_parser)]\n  pub args: Option<JsonObject>,\n}\n\nfn args_parser(args: &str) -> anyhow::Result<JsonObject> {\n  serde_qs::from_str(args).context(\"Failed to parse args\")\n}\n\n/// Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchRunAction {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* actions\n  /// foo-*\n  /// # add some more\n  /// extra-action-1, extra-action-2\n  /// ```\n  pub pattern: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/execute/alerter.rs",
    "content": "use clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{alert::SeverityLevel, update::Update};\n\nuse super::KomodoExecuteRequest;\n\n/// Tests an Alerters ability to reach the configured endpoint. Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct TestAlerter {\n  /// Name or id\n  pub alerter: String,\n}\n\n//\n\n/// Send a custom alert message to configured Alerters. Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct SendAlert {\n  /// The alert level.\n  #[serde(default)]\n  #[clap(long, short = 'l', default_value_t = SeverityLevel::Ok)]\n  pub level: SeverityLevel,\n  /// The alert message. Required.\n  pub message: String,\n  /// The alert details. Optional.\n  #[serde(default)]\n  #[arg(long, short = 'd', default_value_t = String::new())]\n  pub details: String,\n  /// Specific alerter names or ids.\n  /// If empty / not passed, sends to all configured alerters\n  /// with the `Custom` alert type whitelisted / not blacklisted.\n  #[serde(default)]\n  #[arg(long, short = 'a')]\n  pub alerters: Vec<String>,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/execute/build.rs",
    "content": "use clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::update::Update;\n\nuse super::{BatchExecutionResponse, KomodoExecuteRequest};\n\n//\n\n/// Runs the target build. Response: [Update].\n///\n/// 1. Get a handle to the builder. If using AWS builder, this means starting a builder ec2 instance.\n///\n/// 2. Clone the repo on the builder. If an `on_clone` commmand is given, it will be executed.\n///\n/// 3. Execute `docker build {...params}`, where params are determined using the builds configuration.\n///\n/// 4. If a docker registry is configured, the build will be pushed to the registry.\n///\n/// 5. If using AWS builder, destroy the builder ec2 instance.\n///\n/// 6. Deploy any Deployments with *Redeploy on Build* enabled.\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RunBuild {\n  /// Can be build id or name\n  pub build: String,\n}\n\n//\n\n/// Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchRunBuild {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* builds\n  /// foo-*\n  /// # add some more\n  /// extra-build-1, extra-build-2\n  /// ```\n  pub pattern: String,\n}\n\n//\n\n/// Cancels the target build.\n/// Only does anything if the build is `building` when called.\n/// Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct CancelBuild {\n  /// Can be id or name\n  pub build: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/execute/deployment.rs",
    "content": "use clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{TerminationSignal, update::Update};\n\nuse super::{BatchExecutionResponse, KomodoExecuteRequest};\n\n/// Deploys the container for the target deployment. Response: [Update].\n///\n/// 1. Pulls the image onto the target server.\n/// 2. If the container is already running,\n/// it will be stopped and removed using `docker container rm ${container_name}`.\n/// 3. The container will be run using `docker run {...params}`,\n/// where params are determined by the deployment's configuration.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct Deploy {\n  /// Name or id\n  pub deployment: String,\n  /// Override the default termination signal specified in the deployment.\n  /// Only used when deployment needs to be taken down before redeploy.\n  pub stop_signal: Option<TerminationSignal>,\n  /// Override the default termination max time.\n  /// Only used when deployment needs to be taken down before redeploy.\n  pub stop_time: Option<i32>,\n}\n\n//\n\n/// Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchDeploy {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* deployments\n  /// foo-*\n  /// # add some more\n  /// extra-deployment-1, extra-deployment-2\n  /// ```\n  pub pattern: String,\n}\n\n//\n\n/// Pulls the image for the target deployment. Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PullDeployment {\n  /// Name or id\n  pub deployment: String,\n}\n\n//\n\n/// Starts the container for the target deployment. Response: [Update]\n///\n/// 1. Runs `docker start ${container_name}`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct StartDeployment {\n  /// Name or id\n  pub deployment: String,\n}\n\n//\n\n/// Restarts the container for the target deployment. Response: [Update]\n///\n/// 1. Runs `docker restart ${container_name}`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RestartDeployment {\n  /// Name or id\n  pub deployment: String,\n}\n\n//\n\n/// Pauses the container for the target deployment. Response: [Update]\n///\n/// 1. Runs `docker pause ${container_name}`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PauseDeployment {\n  /// Name or id\n  pub deployment: String,\n}\n\n//\n\n/// Unpauses the container for the target deployment. Response: [Update]\n///\n/// 1. Runs `docker unpause ${container_name}`.\n///\n/// Note. This is the only way to restart a paused container.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct UnpauseDeployment {\n  /// Name or id\n  pub deployment: String,\n}\n\n//\n\n/// Stops the container for the target deployment. Response: [Update]\n///\n/// 1. Runs `docker stop ${container_name}`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct StopDeployment {\n  /// Name or id\n  pub deployment: String,\n  /// Override the default termination signal specified in the deployment.\n  pub signal: Option<TerminationSignal>,\n  /// Override the default termination max time.\n  pub time: Option<i32>,\n}\n\n//\n\n/// Stops and destroys the container for the target deployment.\n/// Reponse: [Update].\n///\n/// 1. The container is stopped and removed using `docker container rm ${container_name}`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct DestroyDeployment {\n  /// Name or id.\n  pub deployment: String,\n  /// Override the default termination signal specified in the deployment.\n  pub signal: Option<TerminationSignal>,\n  /// Override the default termination max time.\n  pub time: Option<i32>,\n}\n\n//\n\n/// Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchDestroyDeployment {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* deployments\n  /// foo-*\n  /// # add some more\n  /// extra-deployment-1, extra-deployment-2\n  /// ```\n  pub pattern: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/execute/maintenance.rs",
    "content": "use clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::update::Update;\n\nuse super::KomodoExecuteRequest;\n\n/// Clears all repos from the Core repo cache. Admin only.\n/// Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct ClearRepoCache {}\n\n/// Backs up the Komodo Core database to compressed jsonl files.\n/// Admin only. Response: [Update]\n///\n/// Mount a folder to `/backups`, and Core will use it to create\n/// timestamped database dumps, which can be restored using\n/// the Komodo CLI.\n///\n/// https://komo.do/docs/setup/backup\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct BackupCoreDatabase {}\n\n/// Trigger a global poll for image updates on Stacks and Deployments\n/// with `poll_for_updates` or `auto_update` enabled.\n/// Admin only. Response: [Update]\n///\n/// 1. `docker compose pull` any Stacks / Deployments with `poll_for_updates` or `auto_update` enabled. This will pick up any available updates.\n/// 2. Redeploy Stacks / Deployments that have updates found and 'auto_update' enabled.\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct GlobalAutoUpdate {}\n"
  },
  {
    "path": "client/core/rs/src/api/execute/mod.rs",
    "content": "use clap::{Parser, Subcommand};\nuse derive_variants::EnumVariants;\nuse resolver_api::HasResponse;\nuse serde::{Deserialize, Serialize};\nuse strum::{Display, EnumString};\nuse typeshare::typeshare;\n\nmod action;\nmod alerter;\nmod build;\nmod deployment;\nmod maintenance;\nmod procedure;\nmod repo;\nmod server;\nmod stack;\nmod sync;\n\npub use action::*;\npub use alerter::*;\npub use build::*;\npub use deployment::*;\npub use maintenance::*;\npub use procedure::*;\npub use repo::*;\npub use server::*;\npub use stack::*;\npub use sync::*;\n\nuse crate::{\n  api::write::CommitSync,\n  entities::{_Serror, I64, NoData, update::Update},\n};\n\npub trait KomodoExecuteRequest: HasResponse {}\n\n/// A wrapper for all Komodo exections.\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  EnumVariants,\n  Subcommand,\n)]\n#[variant_derive(\n  Debug,\n  Clone,\n  Copy,\n  Serialize,\n  Deserialize,\n  Display,\n  EnumString\n)]\n#[serde(tag = \"type\", content = \"params\")]\npub enum Execution {\n  /// The \"null\" execution. Does nothing.\n  None(NoData),\n\n  // ACTION\n  /// Run the target action. (alias: `action`, `ac`)\n  #[clap(alias = \"action\", alias = \"ac\")]\n  RunAction(RunAction),\n  BatchRunAction(BatchRunAction),\n\n  // PROCEDURE\n  /// Run the target procedure. (alias: `procedure`, `pr`)\n  #[clap(alias = \"procedure\", alias = \"pr\")]\n  RunProcedure(RunProcedure),\n  BatchRunProcedure(BatchRunProcedure),\n\n  // BUILD\n  /// Run the target build. (alias: `build`, `bd`)\n  #[clap(alias = \"build\", alias = \"bd\")]\n  RunBuild(RunBuild),\n  BatchRunBuild(BatchRunBuild),\n  CancelBuild(CancelBuild),\n\n  // DEPLOYMENT\n  /// Deploy the target deployment. (alias: `dp`)\n  #[clap(alias = \"dp\")]\n  Deploy(Deploy),\n  BatchDeploy(BatchDeploy),\n  PullDeployment(PullDeployment),\n  StartDeployment(StartDeployment),\n  RestartDeployment(RestartDeployment),\n  PauseDeployment(PauseDeployment),\n  UnpauseDeployment(UnpauseDeployment),\n  StopDeployment(StopDeployment),\n  DestroyDeployment(DestroyDeployment),\n  BatchDestroyDeployment(BatchDestroyDeployment),\n\n  // REPO\n  /// Clone the target repo\n  #[clap(alias = \"clone\")]\n  CloneRepo(CloneRepo),\n  BatchCloneRepo(BatchCloneRepo),\n  PullRepo(PullRepo),\n  BatchPullRepo(BatchPullRepo),\n  BuildRepo(BuildRepo),\n  BatchBuildRepo(BatchBuildRepo),\n  CancelRepoBuild(CancelRepoBuild),\n\n  // SERVER (Container)\n  StartContainer(StartContainer),\n  RestartContainer(RestartContainer),\n  PauseContainer(PauseContainer),\n  UnpauseContainer(UnpauseContainer),\n  StopContainer(StopContainer),\n  DestroyContainer(DestroyContainer),\n  StartAllContainers(StartAllContainers),\n  RestartAllContainers(RestartAllContainers),\n  PauseAllContainers(PauseAllContainers),\n  UnpauseAllContainers(UnpauseAllContainers),\n  StopAllContainers(StopAllContainers),\n  PruneContainers(PruneContainers),\n\n  // SERVER (Prune)\n  DeleteNetwork(DeleteNetwork),\n  PruneNetworks(PruneNetworks),\n  DeleteImage(DeleteImage),\n  PruneImages(PruneImages),\n  DeleteVolume(DeleteVolume),\n  PruneVolumes(PruneVolumes),\n  PruneDockerBuilders(PruneDockerBuilders),\n  PruneBuildx(PruneBuildx),\n  PruneSystem(PruneSystem),\n\n  // SYNC\n  /// Execute a Resource Sync. (alias: `sync`)\n  #[clap(alias = \"sync\")]\n  RunSync(RunSync),\n  /// Commit a Resource Sync. (alias: `commit`)\n  #[clap(alias = \"commit\")]\n  CommitSync(CommitSync), // This is a special case, its actually a write operation.\n\n  // STACK\n  /// Deploy the target stack. (alias: `stack`, `st`)\n  #[clap(alias = \"stack\", alias = \"st\")]\n  DeployStack(DeployStack),\n  BatchDeployStack(BatchDeployStack),\n  DeployStackIfChanged(DeployStackIfChanged),\n  BatchDeployStackIfChanged(BatchDeployStackIfChanged),\n  PullStack(PullStack),\n  BatchPullStack(BatchPullStack),\n  StartStack(StartStack),\n  RestartStack(RestartStack),\n  PauseStack(PauseStack),\n  UnpauseStack(UnpauseStack),\n  StopStack(StopStack),\n  DestroyStack(DestroyStack),\n  BatchDestroyStack(BatchDestroyStack),\n  RunStackService(RunStackService),\n\n  // ALERTER\n  TestAlerter(TestAlerter),\n  #[clap(alias = \"alert\")]\n  SendAlert(SendAlert),\n\n  // MAINTENANCE\n  ClearRepoCache(ClearRepoCache),\n  BackupCoreDatabase(BackupCoreDatabase),\n  GlobalAutoUpdate(GlobalAutoUpdate),\n\n  // SLEEP\n  Sleep(Sleep),\n}\n\n/// Sleeps for the specified time.\n#[typeshare]\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Parser)]\npub struct Sleep {\n  #[serde(default)]\n  pub duration_ms: I64,\n}\n\n#[typeshare]\npub type BatchExecutionResponse = Vec<BatchExecutionResponseItem>;\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"status\", content = \"data\")]\npub enum BatchExecutionResponseItem {\n  Ok(Update),\n  Err(BatchExecutionResponseItemErr),\n}\n\nimpl From<Result<Box<Update>, BatchExecutionResponseItemErr>>\n  for BatchExecutionResponseItem\n{\n  fn from(\n    value: Result<Box<Update>, BatchExecutionResponseItemErr>,\n  ) -> Self {\n    match value {\n      Ok(update) => Self::Ok(*update),\n      Err(e) => Self::Err(e),\n    }\n  }\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct BatchExecutionResponseItemErr {\n  pub name: String,\n  pub error: _Serror,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/execute/procedure.rs",
    "content": "use clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::update::Update;\n\nuse super::{BatchExecutionResponse, KomodoExecuteRequest};\n\n/// Runs the target Procedure. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RunProcedure {\n  /// Id or name\n  pub procedure: String,\n}\n\n/// Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchRunProcedure {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* procedures\n  /// foo-*\n  /// # add some more\n  /// extra-procedure-1, extra-procedure-2\n  /// ```\n  pub pattern: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/execute/repo.rs",
    "content": "use clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::update::Update;\n\nuse super::{BatchExecutionResponse, KomodoExecuteRequest};\n\n//\n\n/// Clones the target repo. Response: [Update].\n///\n/// Note. Repo must have server attached at `server_id`.\n///\n/// 1. Clones the repo on the target server using `git clone https://{$token?}@github.com/${repo} -b ${branch}`.\n/// The token will only be used if a github account is specified,\n/// and must be declared in the periphery configuration on the target server.\n/// 2. If `on_clone` and `on_pull` are specified, they will be executed.\n/// `on_clone` will be executed before `on_pull`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct CloneRepo {\n  /// Id or name\n  pub repo: String,\n}\n\n//\n\n/// Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchCloneRepo {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* repos\n  /// foo-*\n  /// # add some more\n  /// extra-repo-1, extra-repo-2\n  /// ```\n  pub pattern: String,\n}\n\n//\n\n/// Pulls the target repo. Response: [Update].\n///\n/// Note. Repo must have server attached at `server_id`.\n///\n/// 1. Pulls the repo on the target server using `git pull`.\n/// 2. If `on_pull` is specified, it will be executed after the pull is complete.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PullRepo {\n  /// Id or name\n  pub repo: String,\n}\n\n//\n\n/// Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchPullRepo {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* repos\n  /// foo-*\n  /// # add some more\n  /// extra-repo-1, extra-repo-2\n  /// ```\n  pub pattern: String,\n}\n\n//\n\n/// Builds the target repo, using the attached builder. Response: [Update].\n///\n/// Note. Repo must have builder attached at `builder_id`.\n///\n/// 1. Spawns the target builder instance (For AWS type. For Server type, just use CloneRepo).\n/// 2. Clones the repo on the builder using `git clone https://{$token?}@github.com/${repo} -b ${branch}`.\n/// The token will only be used if a github account is specified,\n/// and must be declared in the periphery configuration on the builder instance.\n/// 3. If `on_clone` and `on_pull` are specified, they will be executed.\n/// `on_clone` will be executed before `on_pull`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct BuildRepo {\n  /// Id or name\n  pub repo: String,\n}\n\n//\n\n/// Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchBuildRepo {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* repos\n  /// foo-*\n  /// # add some more\n  /// extra-repo-1, extra-repo-2\n  /// ```\n  pub pattern: String,\n}\n\n//\n\n/// Cancels the target repo build.\n/// Only does anything if the repo build is `building` when called.\n/// Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct CancelRepoBuild {\n  /// Can be id or name\n  pub repo: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/execute/server.rs",
    "content": "use clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{TerminationSignal, update::Update};\n\nuse super::KomodoExecuteRequest;\n\n// =============\n// = CONTAINER =\n// =============\n\n/// Starts the container on the target server. Response: [Update]\n///\n/// 1. Runs `docker start ${container_name}`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct StartContainer {\n  /// Name or id\n  pub server: String,\n  /// The container name\n  pub container: String,\n}\n\n//\n\n/// Restarts the container on the target server. Response: [Update]\n///\n/// 1. Runs `docker restart ${container_name}`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RestartContainer {\n  /// Name or id\n  pub server: String,\n  /// The container name\n  pub container: String,\n}\n\n//\n\n/// Pauses the container on the target server. Response: [Update]\n///\n/// 1. Runs `docker pause ${container_name}`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PauseContainer {\n  /// Name or id\n  pub server: String,\n  /// The container name\n  pub container: String,\n}\n\n//\n\n/// Unpauses the container on the target server. Response: [Update]\n///\n/// 1. Runs `docker unpause ${container_name}`.\n///\n/// Note. This is the only way to restart a paused container.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct UnpauseContainer {\n  /// Name or id\n  pub server: String,\n  /// The container name\n  pub container: String,\n}\n\n//\n\n/// Stops the container on the target server. Response: [Update]\n///\n/// 1. Runs `docker stop ${container_name}`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct StopContainer {\n  /// Name or id\n  pub server: String,\n  /// The container name\n  pub container: String,\n  /// Override the default termination signal.\n  pub signal: Option<TerminationSignal>,\n  /// Override the default termination max time.\n  pub time: Option<i32>,\n}\n\n//\n\n/// Stops and destroys the container on the target server.\n/// Reponse: [Update].\n///\n/// 1. The container is stopped and removed using `docker container rm ${container_name}`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct DestroyContainer {\n  /// Name or id\n  pub server: String,\n  /// The container name\n  pub container: String,\n  /// Override the default termination signal.\n  pub signal: Option<TerminationSignal>,\n  /// Override the default termination max time.\n  pub time: Option<i32>,\n}\n\n//\n\n/// Starts all containers on the target server. Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct StartAllContainers {\n  /// Name or id\n  pub server: String,\n}\n\n//\n\n/// Restarts all containers on the target server. Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RestartAllContainers {\n  /// Name or id\n  pub server: String,\n}\n\n//\n\n/// Pauses all containers on the target server. Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PauseAllContainers {\n  /// Name or id\n  pub server: String,\n}\n\n//\n\n/// Unpauses all containers on the target server. Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct UnpauseAllContainers {\n  /// Name or id\n  pub server: String,\n}\n\n//\n\n/// Stops all containers on the target server. Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct StopAllContainers {\n  /// Name or id\n  pub server: String,\n}\n\n//\n\n/// Prunes the docker containers on the target server. Response: [Update].\n///\n/// 1. Runs `docker container prune -f`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PruneContainers {\n  /// Id or name\n  pub server: String,\n}\n\n// ============================\n// = NETWORK / IMAGE / VOLUME =\n// ============================\n\n/// Delete a docker network.\n/// Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct DeleteNetwork {\n  /// Id or name.\n  pub server: String,\n  /// The name of the network to delete.\n  pub name: String,\n}\n\n//\n\n/// Prunes the docker networks on the target server. Response: [Update].\n///\n/// 1. Runs `docker network prune -f`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PruneNetworks {\n  /// Id or name\n  pub server: String,\n}\n\n//\n\n/// Delete a docker image.\n/// Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct DeleteImage {\n  /// Id or name.\n  pub server: String,\n  /// The name of the image to delete.\n  pub name: String,\n}\n\n//\n\n/// Prunes the docker images on the target server. Response: [Update].\n///\n/// 1. Runs `docker image prune -a -f`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PruneImages {\n  /// Id or name\n  pub server: String,\n}\n\n//\n\n/// Delete a docker volume.\n/// Response: [Update]\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct DeleteVolume {\n  /// Id or name.\n  pub server: String,\n  /// The name of the volume to delete.\n  pub name: String,\n}\n\n//\n\n/// Prunes the docker volumes on the target server. Response: [Update].\n///\n/// 1. Runs `docker volume prune -a -f`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PruneVolumes {\n  /// Id or name\n  pub server: String,\n}\n\n//\n\n/// Prunes the docker builders (build cache) on the target server. Response: [Update].\n///\n/// 1. Runs `docker builder prune -a -f`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PruneDockerBuilders {\n  /// Id or name\n  pub server: String,\n}\n\n//\n\n/// Prunes the docker buildx cache on the target server. Response: [Update].\n///\n/// 1. Runs `docker buildx prune -a -f`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PruneBuildx {\n  /// Id or name\n  pub server: String,\n}\n\n//\n\n/// Prunes the docker system on the target server, including volumes. Response: [Update].\n///\n/// 1. Runs `docker system prune -a -f --volumes`.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PruneSystem {\n  /// Id or name\n  pub server: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/execute/stack.rs",
    "content": "use crate::entities::update::Update;\nuse anyhow::Context;\nuse clap::ArgAction::SetTrue;\nuse clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\nuse typeshare::typeshare;\n\nuse super::{BatchExecutionResponse, KomodoExecuteRequest};\n\n/// Deploys the target stack. `docker compose up`. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct DeployStack {\n  /// Id or name\n  pub stack: String,\n  /// Filter to only deploy specific services.\n  /// If empty, will deploy all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n  /// Override the default termination max time.\n  /// Only used if the stack needs to be taken down first.\n  pub stop_time: Option<i32>,\n}\n\n//\n\n/// Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchDeployStack {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* stacks\n  /// foo-*\n  /// # add some more\n  /// extra-stack-1, extra-stack-2\n  /// ```\n  pub pattern: String,\n}\n\n//\n\n/// Checks deployed contents vs latest contents,\n/// and only if any changes found\n/// will `docker compose up`. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct DeployStackIfChanged {\n  /// Id or name\n  pub stack: String,\n  /// Override the default termination max time.\n  /// Only used if the stack needs to be taken down first.\n  pub stop_time: Option<i32>,\n}\n\n//\n\n/// Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchDeployStackIfChanged {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* stacks\n  /// foo-*\n  /// # add some more\n  /// extra-stack-1, extra-stack-2\n  /// ```\n  pub pattern: String,\n}\n\n//\n\n/// Pulls images for the target stack. `docker compose pull`. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PullStack {\n  /// Id or name\n  pub stack: String,\n  /// Filter to only pull specific services.\n  /// If empty, will pull all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n}\n\n//\n\n/// Pulls multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchPullStack {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///\n  /// Example:\n  /// ```text\n  /// # match all foo-* stacks\n  /// foo-*\n  /// # add some more\n  /// extra-stack-1, extra-stack-2\n  /// ```\n  pub pattern: String,\n}\n\n//\n\n/// Starts the target stack. `docker compose start`. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct StartStack {\n  /// Id or name\n  pub stack: String,\n  /// Filter to only start specific services.\n  /// If empty, will start all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n}\n\n//\n\n/// Restarts the target stack. `docker compose restart`. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RestartStack {\n  /// Id or name\n  pub stack: String,\n  /// Filter to only restart specific services.\n  /// If empty, will restart all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n}\n\n//\n\n/// Pauses the target stack. `docker compose pause`. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct PauseStack {\n  /// Id or name\n  pub stack: String,\n  /// Filter to only pause specific services.\n  /// If empty, will pause all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n}\n\n//\n\n/// Unpauses the target stack. `docker compose unpause`. Response: [Update].\n///\n/// Note. This is the only way to restart a paused container.\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct UnpauseStack {\n  /// Id or name\n  pub stack: String,\n  /// Filter to only unpause specific services.\n  /// If empty, will unpause all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n}\n\n//\n\n/// Stops the target stack. `docker compose stop`. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct StopStack {\n  /// Id or name\n  pub stack: String,\n  /// Override the default termination max time.\n  pub stop_time: Option<i32>,\n  /// Filter to only stop specific services.\n  /// If empty, will stop all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n}\n\n//\n\n/// Destoys the target stack. `docker compose down`. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct DestroyStack {\n  /// Id or name\n  pub stack: String,\n  /// Filter to only destroy specific services.\n  /// If empty, will destroy all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n  /// Pass `--remove-orphans`\n  #[serde(default)]\n  pub remove_orphans: bool,\n  /// Override the default termination max time.\n  pub stop_time: Option<i32>,\n}\n\n//\n\n/// Runs a one-time command against a service using `docker compose run`. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RunStackService {\n  /// Id or name\n  pub stack: String,\n  /// Service to run\n  pub service: String,\n  /// Command and args to pass to the service container\n  #[arg(trailing_var_arg = true, num_args = 1.., allow_hyphen_values = true)]\n  pub command: Option<Vec<String>>,\n  /// Do not allocate TTY\n  #[arg(long = \"no-tty\", action = SetTrue)]\n  pub no_tty: Option<bool>,\n  /// Do not start linked services\n  #[arg(long = \"no-deps\", action = SetTrue)]\n  pub no_deps: Option<bool>,\n  /// Detach container on run\n  #[arg(long = \"detach\", action = SetTrue)]\n  pub detach: Option<bool>,\n  /// Map service ports to the host\n  #[arg(long = \"service-ports\", action = SetTrue)]\n  pub service_ports: Option<bool>,\n  /// Extra environment variables for the run\n  #[arg(long = \"env\", short = 'e', value_parser = env_parser)]\n  pub env: Option<HashMap<String, String>>,\n  /// Working directory inside the container\n  #[arg(long = \"workdir\")]\n  pub workdir: Option<String>,\n  /// User to run as inside the container\n  #[arg(long = \"user\")]\n  pub user: Option<String>,\n  /// Override the default entrypoint\n  #[arg(long = \"entrypoint\")]\n  pub entrypoint: Option<String>,\n  /// Pull the image before running\n  #[arg(long = \"pull\", action = SetTrue)]\n  pub pull: Option<bool>,\n}\n\nfn env_parser(args: &str) -> anyhow::Result<HashMap<String, String>> {\n  serde_qs::from_str(args).context(\"Failed to parse env\")\n}\n\n//\n\n/// Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse].\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  PartialEq,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(BatchExecutionResponse)]\n#[error(serror::Error)]\npub struct BatchDestroyStack {\n  /// Id or name or wildcard pattern or regex.\n  /// Supports multiline and comma delineated combinations of the above.\n  ///d\n  /// Example:\n  /// ```text\n  /// # match all foo-* stacks\n  /// foo-*\n  /// # add some more\n  /// extra-stack-1, extra-stack-2\n  /// ```\n  pub pattern: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/execute/sync.rs",
    "content": "use clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{ResourceTargetVariant, update::Update};\n\nuse super::KomodoExecuteRequest;\n\n/// Runs the target resource sync. Response: [Update]\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoExecuteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RunSync {\n  /// Id or name\n  pub sync: String,\n  /// Only execute sync on a specific resource type.\n  /// Combine with `resource_id` to specify resource.\n  pub resource_type: Option<ResourceTargetVariant>,\n  /// Only execute sync on a specific resources.\n  /// Combine with `resource_type` to specify resources.\n  /// Supports name or id.\n  pub resources: Option<Vec<String>>,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/mod.rs",
    "content": "//! # Komodo Core API\n//!\n//! Komodo Core exposes an HTTP api using standard JSON serialization.\n//!\n//! All calls share some common HTTP params:\n//! - Method: `POST`\n//! - Path: `/auth`, `/user`, `/read`, `/write`, `/execute`\n//! - Headers:\n//!   - Content-Type: `application/json`\n//!   - Authorization: `your_jwt`\n//!   - X-Api-Key: `your_api_key`\n//!   - X-Api-Secret: `your_api_secret`\n//!   - Use either Authorization *or* X-Api-Key and X-Api-Secret to authenticate requests.\n//! - Body: JSON specifying the request type (`type`) and the parameters (`params`).\n//!\n//! You can create API keys for your user, or for a Service User with limited permissions,\n//! from the Komodo UI Settings page.\n//!\n//! To call the api, construct JSON bodies following\n//! the schemas given in [read], [mod@write], [execute], and so on.\n//!\n//! For example, this is an example body for [read::GetDeployment]:\n//! ```json\n//! {\n//!   \"type\": \"GetDeployment\",\n//!   \"params\": {\n//!     \"deployment\": \"66113df3abe32960b87018dd\"\n//!   }\n//! }\n//! ```\n//!\n//! The request's parent module (eg. [read], [mod@write]) determines the http path which\n//! must be used for the requests. For example, requests under [read] are made using http path `/read`.\n//!\n//! ## Curl Example\n//!\n//! Putting it all together, here is an example `curl` for [write::UpdateBuild], to update the version:\n//!\n//! ```text\n//! curl --header \"Content-Type: application/json\" \\\n//!     --header \"X-Api-Key: your_api_key\" \\\n//!     --header \"X-Api-Secret: your_api_secret\" \\\n//!     --data '{ \"type\": \"UpdateBuild\", \"params\": { \"id\": \"67076689ed600cfdd52ac637\", \"config\": { \"version\": \"1.15.9\" } } }' \\\n//!     https://komodo.example.com/write\n//! ```\n//!\n//! ## Modules\n//!\n//! - [auth]: Requests relating to logging in / obtaining authentication tokens.\n//! - [user]: User self-management actions (manage api keys, etc.)\n//! - [read]: Read only requests which retrieve data from Komodo.\n//! - [execute]: Run actions on Komodo resources, eg [execute::RunBuild].\n//! - [mod@write]: Requests which alter data, like create / update / delete resources.\n//!\n//! ## Errors\n//!\n//! Request errors will be returned with a JSON body containing information about the error.\n//! They will have the following common format:\n//! ```json\n//! {\n//!   \"error\": \"top level error message\",\n//!   \"trace\": [\n//!     \"first traceback message\",\n//!     \"second traceback message\"\n//!   ]\n//! }\n//! ```\n\npub mod auth;\npub mod execute;\npub mod read;\npub mod terminal;\npub mod user;\npub mod write;\n"
  },
  {
    "path": "client/core/rs/src/api/read/action.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::action::{\n  Action, ActionActionState, ActionListItem, ActionQuery,\n};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get a specific action. Response: [Action].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetActionResponse)]\n#[error(serror::Error)]\npub struct GetAction {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub action: String,\n}\n\n#[typeshare]\npub type GetActionResponse = Action;\n\n//\n\n/// List actions matching optional query. Response: [ListActionsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListActionsResponse)]\n#[error(serror::Error)]\npub struct ListActions {\n  /// optional structured query to filter actions.\n  #[serde(default)]\n  pub query: ActionQuery,\n}\n\n#[typeshare]\npub type ListActionsResponse = Vec<ActionListItem>;\n\n//\n\n/// List actions matching optional query. Response: [ListFullActionsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListFullActionsResponse)]\n#[error(serror::Error)]\npub struct ListFullActions {\n  /// optional structured query to filter actions.\n  #[serde(default)]\n  pub query: ActionQuery,\n}\n\n#[typeshare]\npub type ListFullActionsResponse = Vec<Action>;\n\n//\n\n/// Get current action state for the action. Response: [ActionActionState].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetActionActionStateResponse)]\n#[error(serror::Error)]\npub struct GetActionActionState {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub action: String,\n}\n\n#[typeshare]\npub type GetActionActionStateResponse = ActionActionState;\n\n//\n\n/// Gets a summary of data relating to all actions.\n/// Response: [GetActionsSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetActionsSummaryResponse)]\n#[error(serror::Error)]\npub struct GetActionsSummary {}\n\n/// Response for [GetActionsSummary].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct GetActionsSummaryResponse {\n  /// The total number of actions.\n  pub total: u32,\n  /// The number of actions with Ok state.\n  pub ok: u32,\n  /// The number of actions currently running.\n  pub running: u32,\n  /// The number of actions with failed state.\n  pub failed: u32,\n  /// The number of actions with unknown state.\n  pub unknown: u32,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/read/alert.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{I64, MongoDocument, U64, alert::Alert};\n\nuse super::KomodoReadRequest;\n\n/// Get a paginated list of alerts sorted by timestamp descending.\n/// Response: [ListAlertsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListAlertsResponse)]\n#[error(serror::Error)]\npub struct ListAlerts {\n  /// Pass a custom mongo query to filter the alerts.\n  ///\n  /// ## Example JSON\n  /// ```json\n  /// {\n  ///   \"resolved\": \"false\",\n  ///   \"level\": \"CRITICAL\",\n  ///   \"$or\": [\n  ///     {\n  ///       \"target\": {\n  ///         \"type\": \"Server\",\n  ///         \"id\": \"6608bf89cb2a12b257ab6c09\"\n  ///       }\n  ///     },\n  ///     {\n  ///       \"target\": {\n  ///         \"type\": \"Server\",\n  ///         \"id\": \"660a5f60b74f90d5dae45fa3\"\n  ///       }\n  ///     }\n  ///   ]\n  /// }\n  /// ```\n  /// This will filter to only include open alerts that have CRITICAL level on those two servers.\n  pub query: Option<MongoDocument>,\n  /// Retrieve older results by incrementing the page.\n  /// `page: 0` is default, and returns the most recent results.\n  #[serde(default)]\n  pub page: U64,\n}\n\n/// Response for [ListAlerts].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ListAlertsResponse {\n  pub alerts: Vec<Alert>,\n  /// If more alerts exist, the next page will be given here.\n  /// Otherwise it will be `null`\n  pub next_page: Option<I64>,\n}\n\n//\n\n/// Get an alert: Response: [Alert].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetAlertResponse)]\n#[error(serror::Error)]\npub struct GetAlert {\n  pub id: String,\n}\n\n#[typeshare]\npub type GetAlertResponse = Alert;\n"
  },
  {
    "path": "client/core/rs/src/api/read/alerter.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::alerter::{\n  Alerter, AlerterListItem, AlerterQuery,\n};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get a specific alerter. Response: [Alerter].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetAlerterResponse)]\n#[error(serror::Error)]\npub struct GetAlerter {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub alerter: String,\n}\n\n#[typeshare]\npub type GetAlerterResponse = Alerter;\n\n//\n\n/// List alerters matching optional query. Response: [ListAlertersResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListAlertersResponse)]\n#[error(serror::Error)]\npub struct ListAlerters {\n  /// Structured query to filter alerters.\n  #[serde(default)]\n  pub query: AlerterQuery,\n}\n\n#[typeshare]\npub type ListAlertersResponse = Vec<AlerterListItem>;\n\n/// List full alerters matching optional query. Response: [ListFullAlertersResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListFullAlertersResponse)]\n#[error(serror::Error)]\npub struct ListFullAlerters {\n  /// Structured query to filter alerters.\n  #[serde(default)]\n  pub query: AlerterQuery,\n}\n\n#[typeshare]\npub type ListFullAlertersResponse = Vec<Alerter>;\n\n//\n\n/// Gets a summary of data relating to all alerters.\n/// Response: [GetAlertersSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetAlertersSummaryResponse)]\n#[error(serror::Error)]\npub struct GetAlertersSummary {}\n\n/// Response for [GetAlertersSummary].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetAlertersSummaryResponse {\n  pub total: u32,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/read/build.rs",
    "content": "use std::cmp::Ordering;\n\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  I64, Version,\n  build::{Build, BuildActionState, BuildListItem, BuildQuery},\n};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get a specific build. Response: [Build].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetBuildResponse)]\n#[error(serror::Error)]\npub struct GetBuild {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub build: String,\n}\n\n#[typeshare]\npub type GetBuildResponse = Build;\n\n//\n\n/// List builds matching optional query. Response: [ListBuildsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListBuildsResponse)]\n#[error(serror::Error)]\npub struct ListBuilds {\n  /// optional structured query to filter builds.\n  #[serde(default)]\n  pub query: BuildQuery,\n}\n\n#[typeshare]\npub type ListBuildsResponse = Vec<BuildListItem>;\n\n//\n\n/// List builds matching optional query. Response: [ListFullBuildsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListFullBuildsResponse)]\n#[error(serror::Error)]\npub struct ListFullBuilds {\n  /// optional structured query to filter builds.\n  #[serde(default)]\n  pub query: BuildQuery,\n}\n\n#[typeshare]\npub type ListFullBuildsResponse = Vec<Build>;\n\n//\n\n/// Get current action state for the build. Response: [BuildActionState].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetBuildActionStateResponse)]\n#[error(serror::Error)]\npub struct GetBuildActionState {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub build: String,\n}\n\n#[typeshare]\npub type GetBuildActionStateResponse = BuildActionState;\n\n//\n\n/// Gets a summary of data relating to all builds.\n/// Response: [GetBuildsSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetBuildsSummaryResponse)]\n#[error(serror::Error)]\npub struct GetBuildsSummary {}\n\n/// Response for [GetBuildsSummary].\n#[typeshare]\n#[derive(Serialize, Deserialize, Default, Debug, Clone)]\npub struct GetBuildsSummaryResponse {\n  /// The total number of builds in Komodo.\n  pub total: u32,\n  /// The number of builds with Ok state.\n  pub ok: u32,\n  /// The number of builds with Failed state.\n  pub failed: u32,\n  /// The number of builds currently building.\n  pub building: u32,\n  /// The number of builds with unknown state.\n  pub unknown: u32,\n}\n\n//\n\n/// Gets summary and timeseries breakdown of the last months build count / time for charting.\n/// Response: [GetBuildMonthlyStatsResponse].\n///\n/// Note. This method is paginated. One page is 30 days of data.\n/// Query for older pages by incrementing the page, starting at 0.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetBuildMonthlyStatsResponse)]\n#[error(serror::Error)]\npub struct GetBuildMonthlyStats {\n  /// Query for older data by incrementing the page.\n  /// `page: 0` is the default, and will return the most recent data.\n  #[serde(default)]\n  pub page: u32,\n}\n\n/// Response for [GetBuildMonthlyStats].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct GetBuildMonthlyStatsResponse {\n  pub total_time: f64,  // in hours\n  pub total_count: f64, // number of builds\n  pub days: Vec<BuildStatsDay>,\n}\n\n/// Item in [GetBuildMonthlyStatsResponse]\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct BuildStatsDay {\n  pub time: f64,\n  pub count: f64,\n  pub ts: f64,\n}\n\nimpl GetBuildMonthlyStatsResponse {\n  pub fn new(\n    mut days: Vec<BuildStatsDay>,\n  ) -> GetBuildMonthlyStatsResponse {\n    days.sort_by(|a, b| {\n      if a.ts < b.ts {\n        Ordering::Less\n      } else {\n        Ordering::Greater\n      }\n    });\n    let mut total_time = 0.0;\n    let mut total_count = 0.0;\n    for day in &days {\n      total_time += day.time;\n      total_count += day.count;\n    }\n    GetBuildMonthlyStatsResponse {\n      total_time,\n      total_count,\n      days,\n    }\n  }\n}\n\n//\n\n/// Retrieve versions of the build that were built in the past and available for deployment,\n/// sorted by most recent first.\n/// Response: [ListBuildVersionsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListBuildVersionsResponse)]\n#[error(serror::Error)]\npub struct ListBuildVersions {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub build: String,\n  /// Filter to only include versions matching this major version.\n  pub major: Option<i32>,\n  /// Filter to only include versions matching this minor version.\n  pub minor: Option<i32>,\n  /// Filter to only include versions matching this patch version.\n  pub patch: Option<i32>,\n  /// Limit the number of included results. Default is no limit.\n  pub limit: Option<I64>,\n}\n\n#[typeshare]\npub type ListBuildVersionsResponse = Vec<BuildVersionResponseItem>;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct BuildVersionResponseItem {\n  pub version: Version,\n  pub ts: I64,\n}\n\n//\n\n/// Gets a list of existing values used as extra args across other builds.\n/// Useful to offer suggestions. Response: [ListCommonBuildExtraArgsResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListCommonBuildExtraArgsResponse)]\n#[error(serror::Error)]\npub struct ListCommonBuildExtraArgs {\n  /// optional structured query to filter builds.\n  #[serde(default)]\n  pub query: BuildQuery,\n}\n\n#[typeshare]\npub type ListCommonBuildExtraArgsResponse = Vec<String>;\n\n//\n\n/// Get whether a Build's target repo has a webhook for the build configured. Response: [GetBuildWebhookEnabledResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetBuildWebhookEnabledResponse)]\n#[error(serror::Error)]\npub struct GetBuildWebhookEnabled {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub build: String,\n}\n\n/// Response for [GetBuildWebhookEnabled]\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetBuildWebhookEnabledResponse {\n  /// Whether the repo webhooks can even be managed.\n  /// The repo owner must be in `github_webhook_app.owners` list to be managed.\n  pub managed: bool,\n  /// Whether pushes to branch trigger build. Will always be false if managed is false.\n  pub enabled: bool,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/read/builder.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::builder::{\n  Builder, BuilderListItem, BuilderQuery,\n};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get a specific builder by id or name. Response: [Builder].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetBuilderResponse)]\n#[error(serror::Error)]\npub struct GetBuilder {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub builder: String,\n}\n\n#[typeshare]\npub type GetBuilderResponse = Builder;\n\n//\n\n/// List builders matching structured query. Response: [ListBuildersResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListBuildersResponse)]\n#[error(serror::Error)]\npub struct ListBuilders {\n  #[serde(default)]\n  pub query: BuilderQuery,\n}\n\n#[typeshare]\npub type ListBuildersResponse = Vec<BuilderListItem>;\n\n//\n\n/// List builders matching structured query. Response: [ListFullBuildersResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListFullBuildersResponse)]\n#[error(serror::Error)]\npub struct ListFullBuilders {\n  #[serde(default)]\n  pub query: BuilderQuery,\n}\n\n#[typeshare]\npub type ListFullBuildersResponse = Vec<Builder>;\n\n//\n\n/// Gets a summary of data relating to all builders.\n/// Response: [GetBuildersSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetBuildersSummaryResponse)]\n#[error(serror::Error)]\npub struct GetBuildersSummary {}\n\n/// Response for [GetBuildersSummary].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetBuildersSummaryResponse {\n  /// The total number of builders.\n  pub total: u32,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/read/deployment.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  I64, SearchCombinator, U64,\n  deployment::{\n    Deployment, DeploymentActionState, DeploymentListItem,\n    DeploymentQuery, DeploymentState,\n  },\n  docker::container::{Container, ContainerListItem, ContainerStats},\n  update::Log,\n};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get a specific deployment by name or id. Response: [Deployment].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetDeploymentResponse)]\n#[error(serror::Error)]\npub struct GetDeployment {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub deployment: String,\n}\n\n#[typeshare]\npub type GetDeploymentResponse = Deployment;\n\n//\n\n/// List deployments matching optional query.\n/// Response: [ListDeploymentsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListDeploymentsResponse)]\n#[error(serror::Error)]\npub struct ListDeployments {\n  /// optional structured query to filter deployments.\n  #[serde(default)]\n  pub query: DeploymentQuery,\n}\n\n#[typeshare]\npub type ListDeploymentsResponse = Vec<DeploymentListItem>;\n\n//\n\n/// List deployments matching optional query.\n/// Response: [ListFullDeploymentsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListFullDeploymentsResponse)]\n#[error(serror::Error)]\npub struct ListFullDeployments {\n  /// optional structured query to filter deployments.\n  #[serde(default)]\n  pub query: DeploymentQuery,\n}\n\n#[typeshare]\npub type ListFullDeploymentsResponse = Vec<Deployment>;\n\n//\n\n/// Get the container, including image / status, of the target deployment.\n/// Response: [GetDeploymentContainerResponse].\n///\n/// Note. This does not hit the server directly. The status comes from an\n/// in memory cache on the core, which hits the server periodically\n/// to keep it up to date.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetDeploymentContainerResponse)]\n#[error(serror::Error)]\npub struct GetDeploymentContainer {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub deployment: String,\n}\n\n/// Response for [GetDeploymentContainer].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetDeploymentContainerResponse {\n  pub state: DeploymentState,\n  pub container: Option<ContainerListItem>,\n}\n\n//\n\n/// Inspect the docker container associated with the Deployment.\n/// Response: [Container].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(InspectDeploymentContainerResponse)]\n#[error(serror::Error)]\npub struct InspectDeploymentContainer {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub deployment: String,\n}\n\n#[typeshare]\npub type InspectDeploymentContainerResponse = Container;\n\n//\n\n/// Get the deployment log's tail, split by stdout/stderr.\n/// Response: [Log].\n///\n/// Note. This call will hit the underlying server directly for most up to date log.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetDeploymentLogResponse)]\n#[error(serror::Error)]\npub struct GetDeploymentLog {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub deployment: String,\n  /// The number of lines of the log tail to include.\n  /// Default: 100.\n  /// Max: 5000.\n  #[serde(default = \"default_tail\")]\n  pub tail: U64,\n  /// Enable `--timestamps`\n  #[serde(default)]\n  pub timestamps: bool,\n}\n\nfn default_tail() -> u64 {\n  50\n}\n\n#[typeshare]\npub type GetDeploymentLogResponse = Log;\n\n//\n\n/// Search the deployment log's tail using `grep`. All lines go to stdout.\n/// Response: [Log].\n///\n/// Note. This call will hit the underlying server directly for most up to date log.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(SearchDeploymentLogResponse)]\n#[error(serror::Error)]\npub struct SearchDeploymentLog {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub deployment: String,\n  /// The terms to search for.\n  pub terms: Vec<String>,\n  /// When searching for multiple terms, can use `AND` or `OR` combinator.\n  ///\n  /// - `AND`: Only include lines with **all** terms present in that line.\n  /// - `OR`: Include lines that have one or more matches in the terms.\n  #[serde(default)]\n  pub combinator: SearchCombinator,\n  /// Invert the results, ie return all lines that DON'T match the terms / combinator.\n  #[serde(default)]\n  pub invert: bool,\n  /// Enable `--timestamps`\n  #[serde(default)]\n  pub timestamps: bool,\n}\n\n#[typeshare]\npub type SearchDeploymentLogResponse = Log;\n\n//\n\n/// Get the deployment container's stats using `docker stats`.\n/// Response: [GetDeploymentStatsResponse].\n///\n/// Note. This call will hit the underlying server directly for most up to date stats.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetDeploymentStatsResponse)]\n#[error(serror::Error)]\npub struct GetDeploymentStats {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub deployment: String,\n}\n\n#[typeshare]\npub type GetDeploymentStatsResponse = ContainerStats;\n\n//\n\n/// Get current action state for the deployment.\n/// Response: [DeploymentActionState].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(DeploymentActionState)]\n#[error(serror::Error)]\npub struct GetDeploymentActionState {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub deployment: String,\n}\n\n#[typeshare]\npub type GetDeploymentActionStateResponse = DeploymentActionState;\n\n//\n\n/// Gets a summary of data relating to all deployments.\n/// Response: [GetDeploymentsSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetDeploymentsSummaryResponse)]\n#[error(serror::Error)]\npub struct GetDeploymentsSummary {}\n\n/// Response for [GetDeploymentsSummary].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct GetDeploymentsSummaryResponse {\n  /// The total number of Deployments\n  pub total: I64,\n  /// The number of Deployments with Running state\n  pub running: I64,\n  /// The number of Deployments with Stopped or Paused state\n  pub stopped: I64,\n  /// The number of Deployments with NotDeployed state\n  pub not_deployed: I64,\n  /// The number of Deployments with Restarting or Dead or Created (other) state\n  pub unhealthy: I64,\n  /// The number of Deployments with Unknown state\n  pub unknown: I64,\n}\n\n//\n\n/// Gets a list of existing values used as extra args across other deployments.\n/// Useful to offer suggestions. Response: [ListCommonDeploymentExtraArgsResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListCommonDeploymentExtraArgsResponse)]\n#[error(serror::Error)]\npub struct ListCommonDeploymentExtraArgs {\n  /// optional structured query to filter deployments.\n  #[serde(default)]\n  pub query: DeploymentQuery,\n}\n\n#[typeshare]\npub type ListCommonDeploymentExtraArgsResponse = Vec<String>;\n"
  },
  {
    "path": "client/core/rs/src/api/read/mod.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::{HasResponse, Resolve};\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nmod action;\nmod alert;\nmod alerter;\nmod build;\nmod builder;\nmod deployment;\nmod permission;\nmod procedure;\nmod provider;\nmod repo;\nmod schedule;\nmod server;\nmod stack;\nmod sync;\nmod tag;\nmod toml;\nmod update;\nmod user;\nmod user_group;\nmod variable;\n\npub use action::*;\npub use alert::*;\npub use alerter::*;\npub use build::*;\npub use builder::*;\npub use deployment::*;\npub use permission::*;\npub use procedure::*;\npub use provider::*;\npub use repo::*;\npub use schedule::*;\npub use server::*;\npub use stack::*;\npub use sync::*;\npub use tag::*;\npub use toml::*;\npub use update::*;\npub use user::*;\npub use user_group::*;\npub use variable::*;\n\nuse crate::entities::{\n  ResourceTarget, Timelength,\n  config::{DockerRegistry, GitProvider},\n};\n\npub trait KomodoReadRequest: HasResponse {}\n\n//\n\n/// Get the version of the Komodo Core api.\n/// Response: [GetVersionResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetVersionResponse)]\n#[error(serror::Error)]\npub struct GetVersion {}\n\n/// Response for [GetVersion].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetVersionResponse {\n  /// The version of the core api.\n  pub version: String,\n}\n\n//\n\n/// Get info about the core api configuration.\n/// Response: [GetCoreInfoResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetCoreInfoResponse)]\n#[error(serror::Error)]\npub struct GetCoreInfo {}\n\n/// Response for [GetCoreInfo].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetCoreInfoResponse {\n  /// The title assigned to this core api.\n  pub title: String,\n  /// The monitoring interval of this core api.\n  pub monitoring_interval: Timelength,\n  /// The webhook base url.\n  pub webhook_base_url: String,\n  /// Whether transparent mode is enabled, which gives all users read access to all resources.\n  pub transparent_mode: bool,\n  /// Whether UI write access should be disabled\n  pub ui_write_disabled: bool,\n  /// Whether non admins can create resources\n  pub disable_non_admin_create: bool,\n  /// Whether confirm dialog should be disabled\n  pub disable_confirm_dialog: bool,\n  /// The repo owners for which github webhook management api is available\n  pub github_webhook_owners: Vec<String>,\n  /// Whether to disable websocket automatic reconnect.\n  pub disable_websocket_reconnect: bool,\n  /// Whether to enable fancy toml highlighting.\n  pub enable_fancy_toml: bool,\n  /// TZ identifier Core is using, if manually set.\n  pub timezone: String,\n}\n\n//\n\n/// List the git providers available in Core / Periphery config files.\n/// Response: [ListGitProvidersFromConfigResponse].\n///\n/// Includes:\n///   - providers in core config\n///   - providers configured on builds, repos, syncs\n///   - providers on the optional Server or Builder\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListGitProvidersFromConfigResponse)]\n#[error(serror::Error)]\npub struct ListGitProvidersFromConfig {\n  /// Accepts an optional Server or Builder target to expand the core list with\n  /// providers available on that specific resource.\n  pub target: Option<ResourceTarget>,\n}\n\n#[typeshare]\npub type ListGitProvidersFromConfigResponse = Vec<GitProvider>;\n\n//\n\n/// List the docker registry providers available in Core / Periphery config files.\n/// Response: [ListDockerRegistriesFromConfigResponse].\n///\n/// Includes:\n///   - registries in core config\n///   - registries configured on builds, deployments\n///   - registries on the optional Server or Builder\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListDockerRegistriesFromConfigResponse)]\n#[error(serror::Error)]\npub struct ListDockerRegistriesFromConfig {\n  /// Accepts an optional Server or Builder target to expand the core list with\n  /// providers available on that specific resource.\n  pub target: Option<ResourceTarget>,\n}\n\n#[typeshare]\npub type ListDockerRegistriesFromConfigResponse = Vec<DockerRegistry>;\n\n//\n\n/// List the available secrets from the core config.\n/// Response: [ListSecretsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListSecretsResponse)]\n#[error(serror::Error)]\npub struct ListSecrets {\n  /// Accepts an optional Server or Builder target to expand the core list with\n  /// providers available on that specific resource.\n  pub target: Option<ResourceTarget>,\n}\n\n#[typeshare]\npub type ListSecretsResponse = Vec<String>;\n"
  },
  {
    "path": "client/core/rs/src/api/read/permission.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  ResourceTarget,\n  permission::{Permission, PermissionLevelAndSpecifics, UserTarget},\n};\n\nuse super::KomodoReadRequest;\n\n/// List permissions for the calling user.\n/// Does not include any permissions on UserGroups they may be a part of.\n/// Response: [ListPermissionsResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListPermissionsResponse)]\n#[error(serror::Error)]\npub struct ListPermissions {}\n\n#[typeshare]\npub type ListPermissionsResponse = Vec<Permission>;\n\n//\n\n/// Gets the calling user's permission level on a specific resource.\n/// Factors in any UserGroup's permissions they may be a part of.\n/// Response: [PermissionLevel]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetPermissionResponse)]\n#[error(serror::Error)]\npub struct GetPermission {\n  /// The target to get user permission on.\n  pub target: ResourceTarget,\n}\n\n#[typeshare]\npub type GetPermissionResponse = PermissionLevelAndSpecifics;\n\n//\n\n/// List permissions for a specific user. **Admin only**.\n/// Response: [ListUserTargetPermissionsResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListUserTargetPermissionsResponse)]\n#[error(serror::Error)]\npub struct ListUserTargetPermissions {\n  /// Specify either a user or a user group.\n  pub user_target: UserTarget,\n}\n\n#[typeshare]\npub type ListUserTargetPermissionsResponse = Vec<Permission>;\n"
  },
  {
    "path": "client/core/rs/src/api/read/procedure.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::procedure::{\n  Procedure, ProcedureActionState, ProcedureListItem, ProcedureQuery,\n};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get a specific procedure. Response: [Procedure].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetProcedureResponse)]\n#[error(serror::Error)]\npub struct GetProcedure {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub procedure: String,\n}\n\n#[typeshare]\npub type GetProcedureResponse = Procedure;\n\n//\n\n/// List procedures matching optional query. Response: [ListProceduresResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListProceduresResponse)]\n#[error(serror::Error)]\npub struct ListProcedures {\n  /// optional structured query to filter procedures.\n  #[serde(default)]\n  pub query: ProcedureQuery,\n}\n\n#[typeshare]\npub type ListProceduresResponse = Vec<ProcedureListItem>;\n\n//\n\n/// List procedures matching optional query. Response: [ListFullProceduresResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListFullProceduresResponse)]\n#[error(serror::Error)]\npub struct ListFullProcedures {\n  /// optional structured query to filter procedures.\n  #[serde(default)]\n  pub query: ProcedureQuery,\n}\n\n#[typeshare]\npub type ListFullProceduresResponse = Vec<Procedure>;\n\n//\n\n/// Get current action state for the procedure. Response: [ProcedureActionState].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetProcedureActionStateResponse)]\n#[error(serror::Error)]\npub struct GetProcedureActionState {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub procedure: String,\n}\n\n#[typeshare]\npub type GetProcedureActionStateResponse = ProcedureActionState;\n\n//\n\n/// Gets a summary of data relating to all procedures.\n/// Response: [GetProceduresSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetProceduresSummaryResponse)]\n#[error(serror::Error)]\npub struct GetProceduresSummary {}\n\n/// Response for [GetProceduresSummary].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct GetProceduresSummaryResponse {\n  /// The total number of procedures.\n  pub total: u32,\n  /// The number of procedures with Ok state.\n  pub ok: u32,\n  /// The number of procedures currently running.\n  pub running: u32,\n  /// The number of procedures with failed state.\n  pub failed: u32,\n  /// The number of procedures with unknown state.\n  pub unknown: u32,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/read/provider.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::provider::{\n  DockerRegistryAccount, GitProviderAccount,\n};\n\nuse super::KomodoReadRequest;\n\n/// Get a specific git provider account.\n/// Response: [GetGitProviderAccountResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetGitProviderAccountResponse)]\n#[error(serror::Error)]\npub struct GetGitProviderAccount {\n  pub id: String,\n}\n\n#[typeshare]\npub type GetGitProviderAccountResponse = GitProviderAccount;\n\n//\n\n/// List git provider accounts matching optional query.\n/// Response: [ListGitProviderAccountsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListGitProviderAccountsResponse)]\n#[error(serror::Error)]\npub struct ListGitProviderAccounts {\n  /// Optionally filter by accounts with a specific domain.\n  pub domain: Option<String>,\n  /// Optionally filter by accounts with a specific username.\n  pub username: Option<String>,\n}\n\n#[typeshare]\npub type ListGitProviderAccountsResponse = Vec<GitProviderAccount>;\n\n//\n\n/// Get a specific docker registry account.\n/// Response: [GetDockerRegistryAccountResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetDockerRegistryAccountResponse)]\n#[error(serror::Error)]\npub struct GetDockerRegistryAccount {\n  pub id: String,\n}\n\n#[typeshare]\npub type GetDockerRegistryAccountResponse = DockerRegistryAccount;\n\n//\n\n/// List docker registry accounts matching optional query.\n/// Response: [ListDockerRegistryAccountsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListDockerRegistryAccountsResponse)]\n#[error(serror::Error)]\npub struct ListDockerRegistryAccounts {\n  /// Optionally filter by accounts with a specific domain.\n  pub domain: Option<String>,\n  /// Optionally filter by accounts with a specific username.\n  pub username: Option<String>,\n}\n\n#[typeshare]\npub type ListDockerRegistryAccountsResponse =\n  Vec<DockerRegistryAccount>;\n"
  },
  {
    "path": "client/core/rs/src/api/read/repo.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::repo::{\n  Repo, RepoActionState, RepoListItem, RepoQuery,\n};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get a specific repo. Response: [Repo].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(Repo)]\n#[error(serror::Error)]\npub struct GetRepo {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub repo: String,\n}\n\n#[typeshare]\npub type GetRepoResponse = Repo;\n\n//\n\n/// List repos matching optional query. Response: [ListReposResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListReposResponse)]\n#[error(serror::Error)]\npub struct ListRepos {\n  /// optional structured query to filter repos.\n  #[serde(default)]\n  pub query: RepoQuery,\n}\n\n#[typeshare]\npub type ListReposResponse = Vec<RepoListItem>;\n\n//\n\n/// List repos matching optional query. Response: [ListFullReposResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListFullReposResponse)]\n#[error(serror::Error)]\npub struct ListFullRepos {\n  /// optional structured query to filter repos.\n  #[serde(default)]\n  pub query: RepoQuery,\n}\n\n#[typeshare]\npub type ListFullReposResponse = Vec<Repo>;\n\n//\n\n/// Get current action state for the repo. Response: [RepoActionState].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetRepoActionStateResponse)]\n#[error(serror::Error)]\npub struct GetRepoActionState {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub repo: String,\n}\n\n#[typeshare]\npub type GetRepoActionStateResponse = RepoActionState;\n\n//\n\n/// Gets a summary of data relating to all repos.\n/// Response: [GetReposSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetReposSummaryResponse)]\n#[error(serror::Error)]\npub struct GetReposSummary {}\n\n/// Response for [GetReposSummary]\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct GetReposSummaryResponse {\n  /// The total number of repos\n  pub total: u32,\n  /// The number of repos with Ok state.\n  pub ok: u32,\n  /// The number of repos currently cloning.\n  pub cloning: u32,\n  /// The number of repos currently pulling.\n  pub pulling: u32,\n  /// The number of repos currently building.\n  pub building: u32,\n  /// The number of repos with failed state.\n  pub failed: u32,\n  /// The number of repos with unknown state.\n  pub unknown: u32,\n}\n\n//\n\n/// Get a target Repo's configured webhooks. Response: [GetRepoWebhooksEnabledResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetRepoWebhooksEnabledResponse)]\n#[error(serror::Error)]\npub struct GetRepoWebhooksEnabled {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub repo: String,\n}\n\n/// Response for [GetRepoWebhooksEnabled]\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetRepoWebhooksEnabledResponse {\n  /// Whether the repo webhooks can even be managed.\n  /// The repo owner must be in `github_webhook_app.owners` list to be managed.\n  pub managed: bool,\n  /// Whether pushes to branch trigger clone. Will always be false if managed is false.\n  pub clone_enabled: bool,\n  /// Whether pushes to branch trigger pull. Will always be false if managed is false.\n  pub pull_enabled: bool,\n  /// Whether pushes to branch trigger build. Will always be false if managed is false.\n  pub build_enabled: bool,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/read/schedule.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::{\n  deserializers::string_list_deserializer,\n  entities::{resource::TagQueryBehavior, schedule::Schedule},\n};\n\nuse super::KomodoReadRequest;\n\n/// List configured schedules.\n/// Response: [ListSchedulesResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListSchedulesResponse)]\n#[error(serror::Error)]\npub struct ListSchedules {\n  /// Pass Vec of tag ids or tag names\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  pub tags: Vec<String>,\n  /// 'All' or 'Any'\n  #[serde(default)]\n  pub tag_behavior: TagQueryBehavior,\n}\n\n#[typeshare]\npub type ListSchedulesResponse = Vec<Schedule>;\n"
  },
  {
    "path": "client/core/rs/src/api/read/server.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  I64, ResourceTarget, SearchCombinator, Timelength, U64,\n  docker::{\n    container::{Container, ContainerListItem},\n    image::{Image, ImageHistoryResponseItem, ImageListItem},\n    network::{Network, NetworkListItem},\n    volume::{Volume, VolumeListItem},\n  },\n  server::{\n    Server, ServerActionState, ServerListItem, ServerQuery,\n    ServerState, TerminalInfo,\n  },\n  stack::ComposeProject,\n  stats::{\n    SystemInformation, SystemProcess, SystemStats, SystemStatsRecord,\n  },\n  update::Log,\n};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get a specific server. Response: [Server].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(Server)]\n#[error(serror::Error)]\npub struct GetServer {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n#[typeshare]\npub type GetServerResponse = Server;\n\n//\n\n/// List servers matching optional query. Response: [ListServersResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListServersResponse)]\n#[error(serror::Error)]\npub struct ListServers {\n  /// optional structured query to filter servers.\n  #[serde(default)]\n  pub query: ServerQuery,\n}\n\n#[typeshare]\npub type ListServersResponse = Vec<ServerListItem>;\n\n//\n\n/// List servers matching optional query. Response: [ListFullServersResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListFullServersResponse)]\n#[error(serror::Error)]\npub struct ListFullServers {\n  /// optional structured query to filter servers.\n  #[serde(default)]\n  pub query: ServerQuery,\n}\n\n#[typeshare]\npub type ListFullServersResponse = Vec<Server>;\n\n//\n\n/// Get the state of the target server. Response: [GetServerStateResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetServerStateResponse)]\n#[error(serror::Error)]\npub struct GetServerState {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n/// The response for [GetServerState].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetServerStateResponse {\n  /// The server status.\n  pub status: ServerState,\n}\n\n//\n\n/// Get current action state for the servers. Response: [ServerActionState].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ServerActionState)]\n#[error(serror::Error)]\npub struct GetServerActionState {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n#[typeshare]\npub type GetServerActionStateResponse = ServerActionState;\n\n//\n\n/// Get the version of the Komodo Periphery agent on the target server.\n/// Response: [GetPeripheryVersionResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetPeripheryVersionResponse)]\n#[error(serror::Error)]\npub struct GetPeripheryVersion {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n/// Response for [GetPeripheryVersion].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetPeripheryVersionResponse {\n  /// The version of periphery.\n  pub version: String,\n}\n\n//\n\n/// List the docker networks on the server. Response: [ListDockerNetworksResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListDockerNetworksResponse)]\n#[error(serror::Error)]\npub struct ListDockerNetworks {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n#[typeshare]\npub type ListDockerNetworksResponse = Vec<NetworkListItem>;\n\n//\n\n/// Inspect a docker network on the server. Response: [InspectDockerNetworkResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(InspectDockerNetworkResponse)]\n#[error(serror::Error)]\npub struct InspectDockerNetwork {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n  /// The network name\n  pub network: String,\n}\n\n#[typeshare]\npub type InspectDockerNetworkResponse = Network;\n\n//\n\n/// List the docker images locally cached on the target server.\n/// Response: [ListDockerImagesResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListDockerImagesResponse)]\n#[error(serror::Error)]\npub struct ListDockerImages {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n#[typeshare]\npub type ListDockerImagesResponse = Vec<ImageListItem>;\n\n//\n\n/// Inspect a docker image on the server. Response: [Image].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(InspectDockerImageResponse)]\n#[error(serror::Error)]\npub struct InspectDockerImage {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n  /// The image name\n  pub image: String,\n}\n\n#[typeshare]\npub type InspectDockerImageResponse = Image;\n\n//\n\n/// Get image history from the server. Response: [ListDockerImageHistoryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListDockerImageHistoryResponse)]\n#[error(serror::Error)]\npub struct ListDockerImageHistory {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n  /// The image name\n  pub image: String,\n}\n\n#[typeshare]\npub type ListDockerImageHistoryResponse =\n  Vec<ImageHistoryResponseItem>;\n\n//\n\n/// List all docker containers on the target server.\n/// Response: [ListDockerContainersResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListDockerContainersResponse)]\n#[error(serror::Error)]\npub struct ListDockerContainers {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n#[typeshare]\npub type ListDockerContainersResponse = Vec<ContainerListItem>;\n\n//\n\n/// List all docker containers on the target server.\n/// Response: [ListDockerContainersResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListAllDockerContainersResponse)]\n#[error(serror::Error)]\npub struct ListAllDockerContainers {\n  /// Filter by server id or name.\n  #[serde(default)]\n  pub servers: Vec<String>,\n}\n\n#[typeshare]\npub type ListAllDockerContainersResponse = Vec<ContainerListItem>;\n\n//\n\n/// Gets a summary of data relating to all containers.\n/// Response: [GetDockerContainersSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetDockerContainersSummaryResponse)]\n#[error(serror::Error)]\npub struct GetDockerContainersSummary {}\n\n/// Response for [GetDockerContainersSummary]\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct GetDockerContainersSummaryResponse {\n  /// The total number of Containers\n  pub total: u32,\n  /// The number of Containers with Running state\n  pub running: u32,\n  /// The number of Containers with Stopped or Paused or Created state\n  pub stopped: u32,\n  /// The number of Containers with Restarting or Dead state\n  pub unhealthy: u32,\n  /// The number of Containers with Unknown state\n  pub unknown: u32,\n}\n\n//\n\n/// Inspect a docker container on the server. Response: [Container].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(InspectDockerContainerResponse)]\n#[error(serror::Error)]\npub struct InspectDockerContainer {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n  /// The container name\n  pub container: String,\n}\n\n#[typeshare]\npub type InspectDockerContainerResponse = Container;\n\n//\n\n/// Get the container log's tail, split by stdout/stderr.\n/// Response: [Log].\n///\n/// Note. This call will hit the underlying server directly for most up to date log.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetContainerLogResponse)]\n#[error(serror::Error)]\npub struct GetContainerLog {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n  /// The container name\n  pub container: String,\n  /// The number of lines of the log tail to include.\n  /// Default: 100.\n  /// Max: 5000.\n  #[serde(default = \"default_tail\")]\n  pub tail: U64,\n  /// Enable `--timestamps`\n  #[serde(default)]\n  pub timestamps: bool,\n}\n\nfn default_tail() -> u64 {\n  50\n}\n\n#[typeshare]\npub type GetContainerLogResponse = Log;\n\n//\n\n/// Search the container log's tail using `grep`. All lines go to stdout.\n/// Response: [Log].\n///\n/// Note. This call will hit the underlying server directly for most up to date log.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(SearchContainerLogResponse)]\n#[error(serror::Error)]\npub struct SearchContainerLog {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n  /// The container name\n  pub container: String,\n  /// The terms to search for.\n  pub terms: Vec<String>,\n  /// When searching for multiple terms, can use `AND` or `OR` combinator.\n  ///\n  /// - `AND`: Only include lines with **all** terms present in that line.\n  /// - `OR`: Include lines that have one or more matches in the terms.\n  #[serde(default)]\n  pub combinator: SearchCombinator,\n  /// Invert the results, ie return all lines that DON'T match the terms / combinator.\n  #[serde(default)]\n  pub invert: bool,\n  /// Enable `--timestamps`\n  #[serde(default)]\n  pub timestamps: bool,\n}\n\n#[typeshare]\npub type SearchContainerLogResponse = Log;\n\n//\n\n/// Find the attached resource for a container. Either Deployment or Stack. Response: [GetResourceMatchingContainerResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetResourceMatchingContainerResponse)]\n#[error(serror::Error)]\npub struct GetResourceMatchingContainer {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n  /// The container name\n  pub container: String,\n}\n\n/// Response for [GetResourceMatchingContainer]. Resource is either Deployment, Stack, or None.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetResourceMatchingContainerResponse {\n  pub resource: Option<ResourceTarget>,\n}\n\n//\n\n/// List all docker volumes on the target server.\n/// Response: [ListDockerVolumesResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListDockerVolumesResponse)]\n#[error(serror::Error)]\npub struct ListDockerVolumes {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n#[typeshare]\npub type ListDockerVolumesResponse = Vec<VolumeListItem>;\n\n//\n\n/// Inspect a docker volume on the server. Response: [Volume].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(InspectDockerVolumeResponse)]\n#[error(serror::Error)]\npub struct InspectDockerVolume {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n  /// The volume name\n  pub volume: String,\n}\n\n#[typeshare]\npub type InspectDockerVolumeResponse = Volume;\n\n//\n\n/// List all docker compose projects on the target server.\n/// Response: [ListComposeProjectsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListComposeProjectsResponse)]\n#[error(serror::Error)]\npub struct ListComposeProjects {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n#[typeshare]\npub type ListComposeProjectsResponse = Vec<ComposeProject>;\n\n//\n\n/// Get the system information of the target server.\n/// Response: [SystemInformation].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetSystemInformationResponse)]\n#[error(serror::Error)]\npub struct GetSystemInformation {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n#[typeshare]\npub type GetSystemInformationResponse = SystemInformation;\n\n//\n\n/// Get the system stats on the target server. Response: [SystemStats].\n///\n/// Note. This does not hit the server directly. The stats come from an\n/// in memory cache on the core, which hits the server periodically\n/// to keep it up to date.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetSystemStatsResponse)]\n#[error(serror::Error)]\npub struct GetSystemStats {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n#[typeshare]\npub type GetSystemStatsResponse = SystemStats;\n\n//\n\n/// List the processes running on the target server.\n/// Response: [ListSystemProcessesResponse].\n///\n/// Note. This does not hit the server directly. The procedures come from an\n/// in memory cache on the core, which hits the server periodically\n/// to keep it up to date.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListSystemProcessesResponse)]\n#[error(serror::Error)]\npub struct ListSystemProcesses {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n}\n\n#[typeshare]\npub type ListSystemProcessesResponse = Vec<SystemProcess>;\n\n//\n\n/// Paginated endpoint serving historical (timeseries) server stats for graphing.\n/// Response: [GetHistoricalServerStatsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetHistoricalServerStatsResponse)]\n#[error(serror::Error)]\npub struct GetHistoricalServerStats {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n  /// The granularity of the data.\n  pub granularity: Timelength,\n  /// Page of historical data. Default is 0, which is the most recent data.\n  /// Use with the `next_page` field of the response.\n  #[serde(default)]\n  pub page: u32,\n}\n\n/// Response to [GetHistoricalServerStats].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetHistoricalServerStatsResponse {\n  /// The timeseries page of data.\n  pub stats: Vec<SystemStatsRecord>,\n  /// If there is a next page of data, pass this to `page` to get it.\n  pub next_page: Option<u32>,\n}\n\n//\n\n/// Gets a summary of data relating to all servers.\n/// Response: [GetServersSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetServersSummaryResponse)]\n#[error(serror::Error)]\npub struct GetServersSummary {}\n\n/// Response for [GetServersSummary].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct GetServersSummaryResponse {\n  /// The total number of servers.\n  pub total: I64,\n  /// The number of healthy (`status: OK`) servers.\n  pub healthy: I64,\n  /// The number of servers with warnings (e.g., version mismatch).\n  pub warning: I64,\n  /// The number of unhealthy servers.\n  pub unhealthy: I64,\n  /// The number of disabled servers.\n  pub disabled: I64,\n}\n\n//\n\n/// List the current terminals on specified server.\n/// Response: [ListTerminalsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListTerminalsResponse)]\n#[error(serror::Error)]\npub struct ListTerminals {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub server: String,\n  /// Force a fresh call to Periphery for the list.\n  /// Otherwise the response will be cached for 30s\n  #[serde(default)]\n  pub fresh: bool,\n}\n\n#[typeshare]\npub type ListTerminalsResponse = Vec<TerminalInfo>;\n"
  },
  {
    "path": "client/core/rs/src/api/read/stack.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  SearchCombinator, U64,\n  docker::container::Container,\n  stack::{\n    Stack, StackActionState, StackListItem, StackQuery, StackService,\n  },\n  update::Log,\n};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get a specific stack. Response: [Stack].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetStackResponse)]\n#[error(serror::Error)]\npub struct GetStack {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n}\n\n#[typeshare]\npub type GetStackResponse = Stack;\n\n//\n\n/// Lists a specific stacks services (the containers). Response: [ListStackServicesResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListStackServicesResponse)]\n#[error(serror::Error)]\npub struct ListStackServices {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n}\n\n#[typeshare]\npub type ListStackServicesResponse = Vec<StackService>;\n\n//\n\n/// Inspect the docker container associated with the Stack.\n/// Response: [Container].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(InspectStackContainerResponse)]\n#[error(serror::Error)]\npub struct InspectStackContainer {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n  /// The service name to inspect\n  pub service: String,\n}\n\n#[typeshare]\npub type InspectStackContainerResponse = Container;\n\n//\n\n/// Get a stack's logs. Filter down included services. Response: [GetStackLogResponse].\n///\n/// Note. This call will hit the underlying server directly for most up to date log.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetStackLogResponse)]\n#[error(serror::Error)]\npub struct GetStackLog {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n  /// Filter the logs to only ones from specific services.\n  /// If empty, will include logs from all services.\n  pub services: Vec<String>,\n  /// The number of lines of the log tail to include.\n  /// Default: 100.\n  /// Max: 5000.\n  #[serde(default = \"default_tail\")]\n  pub tail: U64,\n  /// Enable `--timestamps`\n  #[serde(default)]\n  pub timestamps: bool,\n}\n\nfn default_tail() -> u64 {\n  50\n}\n\n#[typeshare]\npub type GetStackLogResponse = Log;\n\n//\n\n/// Search the stack log's tail using `grep`. All lines go to stdout.\n/// Response: [SearchStackLogResponse].\n///\n/// Note. This call will hit the underlying server directly for most up to date log.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(SearchStackLogResponse)]\n#[error(serror::Error)]\npub struct SearchStackLog {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n  /// Filter the logs to only ones from specific services.\n  /// If empty, will include logs from all services.\n  pub services: Vec<String>,\n  /// The terms to search for.\n  pub terms: Vec<String>,\n  /// When searching for multiple terms, can use `AND` or `OR` combinator.\n  ///\n  /// - `AND`: Only include lines with **all** terms present in that line.\n  /// - `OR`: Include lines that have one or more matches in the terms.\n  #[serde(default)]\n  pub combinator: SearchCombinator,\n  /// Invert the results, ie return all lines that DON'T match the terms / combinator.\n  #[serde(default)]\n  pub invert: bool,\n  /// Enable `--timestamps`\n  #[serde(default)]\n  pub timestamps: bool,\n}\n\n#[typeshare]\npub type SearchStackLogResponse = Log;\n\n//\n\n/// Gets a list of existing values used as extra args across other stacks.\n/// Useful to offer suggestions. Response: [ListCommonStackExtraArgsResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListCommonStackExtraArgsResponse)]\n#[error(serror::Error)]\npub struct ListCommonStackExtraArgs {\n  /// optional structured query to filter stacks.\n  #[serde(default)]\n  pub query: StackQuery,\n}\n\n#[typeshare]\npub type ListCommonStackExtraArgsResponse = Vec<String>;\n\n//\n\n/// Gets a list of existing values used as build extra args across other stacks.\n/// Useful to offer suggestions. Response: [ListCommonStackBuildExtraArgsResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListCommonStackBuildExtraArgsResponse)]\n#[error(serror::Error)]\npub struct ListCommonStackBuildExtraArgs {\n  /// optional structured query to filter stacks.\n  #[serde(default)]\n  pub query: StackQuery,\n}\n\n#[typeshare]\npub type ListCommonStackBuildExtraArgsResponse = Vec<String>;\n\n//\n\n/// List stacks matching optional query. Response: [ListStacksResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListStacksResponse)]\n#[error(serror::Error)]\npub struct ListStacks {\n  /// optional structured query to filter stacks.\n  #[serde(default)]\n  pub query: StackQuery,\n}\n\n#[typeshare]\npub type ListStacksResponse = Vec<StackListItem>;\n\n//\n\n/// List stacks matching optional query. Response: [ListFullStacksResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListFullStacksResponse)]\n#[error(serror::Error)]\npub struct ListFullStacks {\n  /// optional structured query to filter stacks.\n  #[serde(default)]\n  pub query: StackQuery,\n}\n\n#[typeshare]\npub type ListFullStacksResponse = Vec<Stack>;\n\n//\n\n/// Get current action state for the stack. Response: [StackActionState].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetStackActionStateResponse)]\n#[error(serror::Error)]\npub struct GetStackActionState {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n}\n\n#[typeshare]\npub type GetStackActionStateResponse = StackActionState;\n\n//\n\n/// Gets a summary of data relating to all syncs.\n/// Response: [GetStacksSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetStacksSummaryResponse)]\n#[error(serror::Error)]\npub struct GetStacksSummary {}\n\n/// Response for [GetStacksSummary]\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct GetStacksSummaryResponse {\n  /// The total number of stacks\n  pub total: u32,\n  /// The number of stacks with Running state.\n  pub running: u32,\n  /// The number of stacks with Stopped or Paused state.\n  pub stopped: u32,\n  /// The number of stacks with Down state.\n  pub down: u32,\n  /// The number of stacks with Unhealthy or Restarting or Dead or Created or Removing state.\n  pub unhealthy: u32,\n  /// The number of stacks with Unknown state.\n  pub unknown: u32,\n}\n\n//\n\n/// Get a target stack's configured webhooks. Response: [GetStackWebhooksEnabledResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetStackWebhooksEnabledResponse)]\n#[error(serror::Error)]\npub struct GetStackWebhooksEnabled {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n}\n\n/// Response for [GetStackWebhooksEnabled]\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetStackWebhooksEnabledResponse {\n  /// Whether the repo webhooks can even be managed.\n  /// The repo owner must be in `github_webhook_app.owners` list to be managed.\n  pub managed: bool,\n  /// Whether pushes to branch trigger refresh. Will always be false if managed is false.\n  pub refresh_enabled: bool,\n  /// Whether pushes to branch trigger stack execution. Will always be false if managed is false.\n  pub deploy_enabled: bool,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/read/sync.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::sync::{\n  ResourceSync, ResourceSyncActionState, ResourceSyncListItem,\n  ResourceSyncQuery,\n};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get a specific sync. Response: [ResourceSync].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ResourceSync)]\n#[error(serror::Error)]\npub struct GetResourceSync {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub sync: String,\n}\n\n#[typeshare]\npub type GetResourceSyncResponse = ResourceSync;\n\n//\n\n/// List syncs matching optional query. Response: [ListResourceSyncsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListResourceSyncsResponse)]\n#[error(serror::Error)]\npub struct ListResourceSyncs {\n  /// optional structured query to filter syncs.\n  #[serde(default)]\n  pub query: ResourceSyncQuery,\n}\n\n#[typeshare]\npub type ListResourceSyncsResponse = Vec<ResourceSyncListItem>;\n\n//\n\n/// List syncs matching optional query. Response: [ListFullResourceSyncsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListFullResourceSyncsResponse)]\n#[error(serror::Error)]\npub struct ListFullResourceSyncs {\n  /// optional structured query to filter syncs.\n  #[serde(default)]\n  pub query: ResourceSyncQuery,\n}\n\n#[typeshare]\npub type ListFullResourceSyncsResponse = Vec<ResourceSync>;\n\n//\n\n/// Get current action state for the sync. Response: [ResourceSyncActionState].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetResourceSyncActionStateResponse)]\n#[error(serror::Error)]\npub struct GetResourceSyncActionState {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub sync: String,\n}\n\n#[typeshare]\npub type GetResourceSyncActionStateResponse = ResourceSyncActionState;\n\n//\n\n/// Gets a summary of data relating to all syncs.\n/// Response: [GetResourceSyncsSummaryResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetResourceSyncsSummaryResponse)]\n#[error(serror::Error)]\npub struct GetResourceSyncsSummary {}\n\n/// Response for [GetResourceSyncsSummary]\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct GetResourceSyncsSummaryResponse {\n  /// The total number of syncs\n  pub total: u32,\n  /// The number of syncs with Ok state.\n  pub ok: u32,\n  /// The number of syncs currently syncing.\n  pub syncing: u32,\n  /// The number of syncs with pending updates\n  pub pending: u32,\n  /// The number of syncs with failed state.\n  pub failed: u32,\n  /// The number of syncs with unknown state.\n  pub unknown: u32,\n}\n\n//\n\n/// Get a target Sync's configured webhooks. Response: [GetSyncWebhooksEnabledResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetSyncWebhooksEnabledResponse)]\n#[error(serror::Error)]\npub struct GetSyncWebhooksEnabled {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub sync: String,\n}\n\n/// Response for [GetSyncWebhooksEnabled]\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetSyncWebhooksEnabledResponse {\n  /// Whether the repo webhooks can even be managed.\n  /// The repo owner must be in `github_webhook_app.owners` list to be managed.\n  pub managed: bool,\n  /// Whether pushes to branch trigger refresh. Will always be false if managed is false.\n  pub refresh_enabled: bool,\n  /// Whether pushes to branch trigger sync execution. Will always be false if managed is false.\n  pub sync_enabled: bool,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/read/tag.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{MongoDocument, tag::Tag};\n\nuse super::KomodoReadRequest;\n\n//\n\n/// Get data for a specific tag. Response [Tag].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetTagResponse)]\n#[error(serror::Error)]\npub struct GetTag {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub tag: String,\n}\n\n#[typeshare]\npub type GetTagResponse = Tag;\n\n//\n\n/// List data for tags matching optional mongo query.\n/// Response: [ListTagsResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListTagsResponse)]\n#[error(serror::Error)]\npub struct ListTags {\n  pub query: Option<MongoDocument>,\n}\n\n#[typeshare]\npub type ListTagsResponse = Vec<Tag>;\n"
  },
  {
    "path": "client/core/rs/src/api/read/toml.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::ResourceTarget;\n\nuse super::KomodoReadRequest;\n\n/// Response containing pretty formatted toml contents.\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct TomlResponse {\n  pub toml: String,\n}\n\n//\n\n/// Get pretty formatted monrun sync toml for all resources\n/// which the user has permissions to view.\n/// Response: [TomlResponse].\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, Serialize, Deserialize, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ExportAllResourcesToTomlResponse)]\n#[error(serror::Error)]\npub struct ExportAllResourcesToToml {\n  /// Whether to include any resources (servers, stacks, etc.)\n  /// in the exported contents.\n  /// Default: `true`\n  #[serde(default = \"default_include_resources\")]\n  pub include_resources: bool,\n  /// Filter resources by tag.\n  /// Accepts tag name or id. Empty array will not filter by tag.\n  #[serde(default)]\n  pub tags: Vec<String>,\n  /// Whether to include variables in the exported contents.\n  /// Default: false\n  #[serde(default)]\n  pub include_variables: bool,\n  /// Whether to include user groups in the exported contents.\n  /// Default: false\n  #[serde(default)]\n  pub include_user_groups: bool,\n}\n\nfn default_include_resources() -> bool {\n  true\n}\n\n#[typeshare]\npub type ExportAllResourcesToTomlResponse = TomlResponse;\n\n//\n\n/// Get pretty formatted monrun sync toml for specific resources and user groups.\n/// Response: [TomlResponse].\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, Serialize, Deserialize, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ExportResourcesToTomlResponse)]\n#[error(serror::Error)]\npub struct ExportResourcesToToml {\n  /// The targets to include in the export.\n  #[serde(default)]\n  pub targets: Vec<ResourceTarget>,\n  /// The user group names or ids to include in the export.\n  #[serde(default)]\n  pub user_groups: Vec<String>,\n  /// Whether to include variables\n  #[serde(default)]\n  pub include_variables: bool,\n}\n\n#[typeshare]\npub type ExportResourcesToTomlResponse = TomlResponse;\n"
  },
  {
    "path": "client/core/rs/src/api/read/update.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  MongoDocument,\n  update::{Update, UpdateListItem},\n};\n\nuse super::KomodoReadRequest;\n\n/// Get all data for the target update.\n/// Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetUpdateResponse)]\n#[error(serror::Error)]\npub struct GetUpdate {\n  /// The update id.\n  pub id: String,\n}\n\n#[typeshare]\npub type GetUpdateResponse = Update;\n\n//\n\n/// Paginated endpoint for updates matching optional query.\n/// More recent updates will be returned first.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListUpdatesResponse)]\n#[error(serror::Error)]\npub struct ListUpdates {\n  /// An optional mongo query to filter the updates.\n  pub query: Option<MongoDocument>,\n  /// Page of updates. Default is 0, which is the most recent data.\n  /// Use with the `next_page` field of the response.\n  #[serde(default)]\n  pub page: u32,\n}\n\n/// Response for [ListUpdates].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ListUpdatesResponse {\n  /// The page of updates, sorted by timestamp descending.\n  pub updates: Vec<UpdateListItem>,\n  /// If there is a next page of data, pass this to `page` to get it.\n  pub next_page: Option<u32>,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/read/user.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{api_key::ApiKey, user::User};\n\nuse super::KomodoReadRequest;\n\n/// Gets list of api keys for the calling user.\n/// Response: [ListApiKeysResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListApiKeysResponse)]\n#[error(serror::Error)]\npub struct ListApiKeys {}\n\n#[typeshare]\npub type ListApiKeysResponse = Vec<ApiKey>;\n\n//\n\n/// **Admin only.**\n/// Gets list of api keys for the user.\n/// Will still fail if you call for a user_id that isn't a service user.\n/// Response: [ListApiKeysForServiceUserResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListApiKeysForServiceUserResponse)]\n#[error(serror::Error)]\npub struct ListApiKeysForServiceUser {\n  /// Id or username\n  #[serde(alias = \"id\", alias = \"username\")]\n  pub user: String,\n}\n\n#[typeshare]\npub type ListApiKeysForServiceUserResponse = Vec<ApiKey>;\n\n//\n\n/// **Admin only.**\n/// Find a user.\n/// Response: [FindUserResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(FindUserResponse)]\n#[error(serror::Error)]\npub struct FindUser {\n  /// Id or username\n  #[serde(alias = \"id\", alias = \"username\")]\n  pub user: String,\n}\n\n#[typeshare]\npub type FindUserResponse = User;\n\n//\n\n/// **Admin only.**\n/// Gets list of Komodo users.\n/// Response: [ListUsersResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListUsersResponse)]\n#[error(serror::Error)]\npub struct ListUsers {}\n\n#[typeshare]\npub type ListUsersResponse = Vec<User>;\n\n//\n\n/// Gets the username of a specific user.\n/// Response: [GetUsernameResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetUsernameResponse)]\n#[error(serror::Error)]\npub struct GetUsername {\n  /// The id of the user.\n  pub user_id: String,\n}\n\n/// Response for [GetUsername].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetUsernameResponse {\n  /// The username of the user.\n  pub username: String,\n  /// An optional icon for the user.\n  pub avatar: Option<String>,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/read/user_group.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::user_group::UserGroup;\n\nuse super::KomodoReadRequest;\n\n/// Get a specific user group by name or id.\n/// Response: [UserGroup].\n#[typeshare]\n#[derive(\n  Debug, Clone, Serialize, Deserialize, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetUserGroupResponse)]\n#[error(serror::Error)]\npub struct GetUserGroup {\n  /// Name or Id\n  pub user_group: String,\n}\n\n#[typeshare]\npub type GetUserGroupResponse = UserGroup;\n\n//\n\n/// List all user groups which user can see. Response: [ListUserGroupsResponse].\n///\n/// Admins can see all user groups,\n/// and users can see user groups to which they belong.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, Serialize, Deserialize, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListUserGroupsResponse)]\n#[error(serror::Error)]\npub struct ListUserGroups {}\n\n#[typeshare]\npub type ListUserGroupsResponse = Vec<UserGroup>;\n"
  },
  {
    "path": "client/core/rs/src/api/read/variable.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::variable::Variable;\n\nuse super::KomodoReadRequest;\n\n/// List all available global variables.\n/// Response: [Variable]\n///\n/// Note. For non admin users making this call,\n/// secret variables will have their values obscured.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(GetVariableResponse)]\n#[error(serror::Error)]\npub struct GetVariable {\n  /// The name of the variable to get.\n  pub name: String,\n}\n\n#[typeshare]\npub type GetVariableResponse = Variable;\n\n//\n\n/// List all available global variables.\n/// Response: [ListVariablesResponse]\n///\n/// Note. For non admin users making this call,\n/// secret variables will have their values obscured.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoReadRequest)]\n#[response(ListVariablesResponse)]\n#[error(serror::Error)]\npub struct ListVariables {}\n\n#[typeshare]\npub type ListVariablesResponse = Vec<Variable>;\n"
  },
  {
    "path": "client/core/rs/src/api/terminal.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\n/// Query to connect to a terminal (interactive shell over websocket) on the given server.\n/// TODO: Document calling.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ConnectTerminalQuery {\n  /// Server Id or name\n  pub server: String,\n  /// Each periphery can keep multiple terminals open.\n  /// If a terminals with the specified name does not exist,\n  /// the call will fail.\n  /// Create a terminal using [CreateTerminal][super::write::server::CreateTerminal]\n  pub terminal: String,\n}\n\n/// Execute a terminal command on the given server.\n/// TODO: Document calling.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ExecuteTerminalBody {\n  /// Server Id or name\n  pub server: String,\n  /// The name of the terminal on the server to use to execute.\n  /// If the terminal at name exists, it will be used to execute the command.\n  /// Otherwise, a new terminal will be created for this command, which will\n  /// persist until it exits or is deleted.\n  pub terminal: String,\n  /// The command to execute.\n  pub command: String,\n}\n\n/// Query to connect to a container exec session (interactive shell over websocket) on the given server.\n/// TODO: Document calling.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ConnectContainerExecQuery {\n  /// Server Id or name\n  pub server: String,\n  /// The container name\n  pub container: String,\n  /// The shell to use (eg. `sh` or `bash`)\n  pub shell: String,\n}\n\n/// Execute a command in the given containers shell.\n/// TODO: Document calling.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ExecuteContainerExecBody {\n  /// Server Id or name\n  pub server: String,\n  /// The container name\n  pub container: String,\n  /// The shell to use (eg. `sh` or `bash`)\n  pub shell: String,\n  /// The command to execute.\n  pub command: String,\n}\n\n/// Query to connect to a container exec session (interactive shell over websocket) on the given Deployment.\n/// This call will use access to the Deployment Terminal to permission the call.\n/// TODO: Document calling.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ConnectDeploymentExecQuery {\n  /// Deployment Id or name\n  pub deployment: String,\n  /// The shell to use (eg. `sh` or `bash`)\n  pub shell: String,\n}\n\n/// Execute a command in the given containers shell.\n/// TODO: Document calling.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ExecuteDeploymentExecBody {\n  /// Deployment Id or name\n  pub deployment: String,\n  /// The shell to use (eg. `sh` or `bash`)\n  pub shell: String,\n  /// The command to execute.\n  pub command: String,\n}\n\n/// Query to connect to a container exec session (interactive shell over websocket) on the given Stack / service.\n/// This call will use access to the Stack Terminal to permission the call.\n/// TODO: Document calling.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ConnectStackExecQuery {\n  /// Stack Id or name\n  pub stack: String,\n  /// The service name to connect to\n  pub service: String,\n  /// The shell to use (eg. `sh` or `bash`)\n  pub shell: String,\n}\n\n/// Execute a command in the given containers shell.\n/// TODO: Document calling.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ExecuteStackExecBody {\n  /// Stack Id or name\n  pub stack: String,\n  /// The service name to connect to\n  pub service: String,\n  /// The shell to use (eg. `sh` or `bash`)\n  pub shell: String,\n  /// The command to execute.\n  pub command: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/user.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::{HasResponse, Resolve};\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{I64, NoData, ResourceTarget};\n\npub trait KomodoUserRequest: HasResponse {}\n\n//\n\n/// Push a resource to the front of the users 10 most recently viewed resources.\n/// Response: [NoData].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoUserRequest)]\n#[response(PushRecentlyViewedResponse)]\n#[error(serror::Error)]\npub struct PushRecentlyViewed {\n  /// The target to push.\n  pub resource: ResourceTarget,\n}\n\n#[typeshare]\npub type PushRecentlyViewedResponse = NoData;\n\n//\n\n/// Set the time the user last opened the UI updates.\n/// Used for unseen notification dot.\n/// Response: [NoData]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoUserRequest)]\n#[response(SetLastSeenUpdateResponse)]\n#[error(serror::Error)]\npub struct SetLastSeenUpdate {}\n\n#[typeshare]\npub type SetLastSeenUpdateResponse = NoData;\n\n//\n\n/// Create an api key for the calling user.\n/// Response: [CreateApiKeyResponse].\n///\n/// Note. After the response is served, there will be no way\n/// to get the secret later.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoUserRequest)]\n#[response(CreateApiKeyResponse)]\n#[error(serror::Error)]\npub struct CreateApiKey {\n  /// The name for the api key.\n  pub name: String,\n\n  /// A unix timestamp in millseconds specifying api key expire time.\n  /// Default is 0, which means no expiry.\n  #[serde(default)]\n  pub expires: I64,\n}\n\n/// Response for [CreateApiKey].\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct CreateApiKeyResponse {\n  /// X-API-KEY\n  pub key: String,\n\n  /// X-API-SECRET\n  ///\n  /// Note.\n  /// There is no way to get the secret again after it is distributed in this message\n  pub secret: String,\n}\n\n//\n\n/// Delete an api key for the calling user.\n/// Response: [NoData]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoUserRequest)]\n#[response(DeleteApiKeyResponse)]\n#[error(serror::Error)]\npub struct DeleteApiKey {\n  /// The key which the user intends to delete.\n  pub key: String,\n}\n\n#[typeshare]\npub type DeleteApiKeyResponse = NoData;\n"
  },
  {
    "path": "client/core/rs/src/api/write/action.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  NoData,\n  action::{_PartialActionConfig, Action},\n  update::Update,\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create a action. Response: [Action].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Action)]\n#[error(serror::Error)]\npub struct CreateAction {\n  /// The name given to newly created action.\n  pub name: String,\n  /// Optional partial config to initialize the action with.\n  #[serde(default)]\n  pub config: _PartialActionConfig,\n}\n\n//\n\n/// Creates a new action with given `name` and the configuration\n/// of the action at the given `id`. Response: [Action].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Action)]\n#[error(serror::Error)]\npub struct CopyAction {\n  /// The name of the new action.\n  pub name: String,\n  /// The id of the action to copy.\n  pub id: String,\n}\n\n//\n\n/// Deletes the action at the given id, and returns the deleted action.\n/// Response: [Action]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Action)]\n#[error(serror::Error)]\npub struct DeleteAction {\n  /// The id or name of the action to delete.\n  pub id: String,\n}\n\n//\n\n/// Update the action at the given id, and return the updated action.\n/// Response: [Action].\n///\n/// Note. This method updates only the fields which are set in the [_PartialActionConfig],\n/// effectively merging diffs into the final document.\n/// This is helpful when multiple users are using\n/// the same resources concurrently by ensuring no unintentional\n/// field changes occur from out of date local state.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Action)]\n#[error(serror::Error)]\npub struct UpdateAction {\n  /// The id of the action to update.\n  pub id: String,\n  /// The partial config update to apply.\n  pub config: _PartialActionConfig,\n}\n\n//\n\n/// Rename the Action at id to the given name.\n/// Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RenameAction {\n  /// The id or name of the Action to rename.\n  pub id: String,\n  /// The new name.\n  pub name: String,\n}\n\n/// Create a webhook on the github action attached to the Action resource.\n/// passed in request. Response: [CreateActionWebhookResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateActionWebhookResponse)]\n#[error(serror::Error)]\npub struct CreateActionWebhook {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub action: String,\n}\n\n#[typeshare]\npub type CreateActionWebhookResponse = NoData;\n\n//\n\n/// Delete the webhook on the github action attached to the Action resource.\n/// passed in request. Response: [DeleteActionWebhookResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteActionWebhookResponse)]\n#[error(serror::Error)]\npub struct DeleteActionWebhook {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub action: String,\n}\n\n#[typeshare]\npub type DeleteActionWebhookResponse = NoData;\n"
  },
  {
    "path": "client/core/rs/src/api/write/alerter.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  alerter::{_PartialAlerterConfig, Alerter},\n  update::Update,\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create an alerter. Response: [Alerter].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Alerter)]\n#[error(serror::Error)]\npub struct CreateAlerter {\n  /// The name given to newly created alerter.\n  pub name: String,\n  /// Optional partial config to initialize the alerter with.\n  #[serde(default)]\n  pub config: _PartialAlerterConfig,\n}\n\n//\n\n/// Creates a new alerter with given `name` and the configuration\n/// of the alerter at the given `id`. Response: [Alerter].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Alerter)]\n#[error(serror::Error)]\npub struct CopyAlerter {\n  /// The name of the new alerter.\n  pub name: String,\n  /// The id of the alerter to copy.\n  pub id: String,\n}\n\n//\n\n/// Deletes the alerter at the given id, and returns the deleted alerter.\n/// Response: [Alerter]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Alerter)]\n#[error(serror::Error)]\npub struct DeleteAlerter {\n  /// The id or name of the alerter to delete.\n  pub id: String,\n}\n\n//\n\n/// Update the alerter at the given id, and return the updated alerter. Response: [Alerter].\n///\n/// Note. This method updates only the fields which are set in the [PartialAlerterConfig][crate::entities::alerter::PartialAlerterConfig],\n/// effectively merging diffs into the final document. This is helpful when multiple users are using\n/// the same resources concurrently by ensuring no unintentional\n/// field changes occur from out of date local state.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Alerter)]\n#[error(serror::Error)]\npub struct UpdateAlerter {\n  /// The id of the alerter to update.\n  pub id: String,\n  /// The partial config update to apply.\n  pub config: _PartialAlerterConfig,\n}\n\n//\n\n/// Rename the Alerter at id to the given name.\n/// Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RenameAlerter {\n  /// The id or name of the Alerter to rename.\n  pub id: String,\n  /// The new name.\n  pub name: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/write/api_key.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::{\n  api::user::CreateApiKeyResponse,\n  entities::{I64, NoData},\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Admin only method to create an api key for a service user.\n/// Response: [CreateApiKeyResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateApiKeyForServiceUserResponse)]\n#[error(serror::Error)]\npub struct CreateApiKeyForServiceUser {\n  /// Must be service user\n  pub user_id: String,\n  /// The name for the api key\n  pub name: String,\n  /// A unix timestamp in millseconds specifying api key expire time.\n  /// Default is 0, which means no expiry.\n  #[serde(default)]\n  pub expires: I64,\n}\n\n#[typeshare]\npub type CreateApiKeyForServiceUserResponse = CreateApiKeyResponse;\n\n//\n\n/// Admin only method to delete an api key for a service user.\n/// Response: [NoData].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteApiKeyForServiceUserResponse)]\n#[error(serror::Error)]\npub struct DeleteApiKeyForServiceUser {\n  pub key: String,\n}\n\n#[typeshare]\npub type DeleteApiKeyForServiceUserResponse = NoData;\n"
  },
  {
    "path": "client/core/rs/src/api/write/build.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  NoData,\n  build::{_PartialBuildConfig, Build},\n  update::Update,\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create a build. Response: [Build].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Build)]\n#[error(serror::Error)]\npub struct CreateBuild {\n  /// The name given to newly created build.\n  pub name: String,\n  /// Optional partial config to initialize the build with.\n  #[serde(default)]\n  pub config: _PartialBuildConfig,\n}\n\n//\n\n/// Creates a new build with given `name` and the configuration\n/// of the build at the given `id`. Response: [Build].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Build)]\n#[error(serror::Error)]\npub struct CopyBuild {\n  /// The name of the new build.\n  pub name: String,\n  /// The id of the build to copy.\n  pub id: String,\n}\n\n//\n\n/// Deletes the build at the given id, and returns the deleted build.\n/// Response: [Build]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Build)]\n#[error(serror::Error)]\npub struct DeleteBuild {\n  /// The id or name of the build to delete.\n  pub id: String,\n}\n\n//\n\n/// Update the build at the given id, and return the updated build.\n/// Response: [Build].\n///\n/// Note. This method updates only the fields which are set in the [_PartialBuildConfig],\n/// effectively merging diffs into the final document.\n/// This is helpful when multiple users are using\n/// the same resources concurrently by ensuring no unintentional\n/// field changes occur from out of date local state.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Build)]\n#[error(serror::Error)]\npub struct UpdateBuild {\n  /// The id or name of the build to update.\n  pub id: String,\n  /// The partial config update to apply.\n  pub config: _PartialBuildConfig,\n}\n\n//\n\n/// Rename the Build at id to the given name.\n/// Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RenameBuild {\n  /// The id or name of the Build to rename.\n  pub id: String,\n  /// The new name.\n  pub name: String,\n}\n\n//\n\n/// Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct WriteBuildFileContents {\n  /// The name or id of the target Build.\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub build: String,\n  /// The dockerfile contents to write.\n  pub contents: String,\n}\n\n//\n\n/// Trigger a refresh of the cached latest hash and message.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(NoData)]\n#[error(serror::Error)]\npub struct RefreshBuildCache {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub build: String,\n}\n\n//\n\n/// Create a webhook on the github repo attached to the build\n/// passed in request. Response: [CreateBuildWebhookResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateBuildWebhookResponse)]\n#[error(serror::Error)]\npub struct CreateBuildWebhook {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub build: String,\n}\n\n#[typeshare]\npub type CreateBuildWebhookResponse = NoData;\n\n//\n\n/// Delete a webhook on the github repo attached to the build\n/// passed in request. Response: [CreateBuildWebhookResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteBuildWebhookResponse)]\n#[error(serror::Error)]\npub struct DeleteBuildWebhook {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub build: String,\n}\n\n#[typeshare]\npub type DeleteBuildWebhookResponse = NoData;\n"
  },
  {
    "path": "client/core/rs/src/api/write/builder.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  builder::{Builder, PartialBuilderConfig},\n  update::Update,\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create a builder. Response: [Builder].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Builder)]\n#[error(serror::Error)]\npub struct CreateBuilder {\n  /// The name given to newly created builder.\n  pub name: String,\n  /// Optional partial config to initialize the builder with.\n  #[serde(default)]\n  pub config: PartialBuilderConfig,\n}\n\n//\n\n/// Creates a new builder with given `name` and the configuration\n/// of the builder at the given `id`. Response: [Builder]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Builder)]\n#[error(serror::Error)]\npub struct CopyBuilder {\n  /// The name of the new builder.\n  pub name: String,\n  /// The id of the builder to copy.\n  pub id: String,\n}\n\n//\n\n/// Deletes the builder at the given id, and returns the deleted builder.\n/// Response: [Builder]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Builder)]\n#[error(serror::Error)]\npub struct DeleteBuilder {\n  /// The id or name of the builder to delete.\n  pub id: String,\n}\n\n//\n\n/// Update the builder at the given id, and return the updated builder.\n/// Response: [Builder].\n///\n/// Note. This method updates only the fields which are set in the [PartialBuilderConfig],\n/// effectively merging diffs into the final document.\n/// This is helpful when multiple users are using\n/// the same resources concurrently by ensuring no unintentional\n/// field changes occur from out of date local state.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Builder)]\n#[error(serror::Error)]\npub struct UpdateBuilder {\n  /// The id of the builder to update.\n  pub id: String,\n  /// The partial config update to apply.\n  pub config: PartialBuilderConfig,\n}\n\n//\n\n/// Rename the Builder at id to the given name.\n/// Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RenameBuilder {\n  /// The id or name of the Builder to rename.\n  pub id: String,\n  /// The new name.\n  pub name: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/write/deployment.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  deployment::{_PartialDeploymentConfig, Deployment},\n  update::Update,\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create a deployment. Response: [Deployment].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Deployment)]\n#[error(serror::Error)]\npub struct CreateDeployment {\n  /// The name given to newly created deployment.\n  pub name: String,\n  /// Optional partial config to initialize the deployment with.\n  #[serde(default)]\n  pub config: _PartialDeploymentConfig,\n}\n\n//\n\n/// Creates a new deployment with given `name` and the configuration\n/// of the deployment at the given `id`. Response: [Deployment]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Deployment)]\n#[error(serror::Error)]\npub struct CopyDeployment {\n  /// The name of the new deployment.\n  pub name: String,\n  /// The id of the deployment to copy.\n  pub id: String,\n}\n\n//\n\n/// Create a Deployment from an existing container. Response: [Deployment].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Deployment)]\n#[error(serror::Error)]\npub struct CreateDeploymentFromContainer {\n  /// The name or id of the existing container.\n  pub name: String,\n  /// The server id or name on which container exists.\n  pub server: String,\n}\n\n//\n\n/// Deletes the deployment at the given id, and returns the deleted deployment.\n/// Response: [Deployment].\n///\n/// Note. If the associated container is running, it will be deleted as part of\n/// the deployment clean up.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Deployment)]\n#[error(serror::Error)]\npub struct DeleteDeployment {\n  /// The id or name of the deployment to delete.\n  pub id: String,\n}\n\n//\n\n/// Update the deployment at the given id, and return the updated deployment.\n/// Response: [Deployment].\n///\n/// Note. If the attached server for the deployment changes,\n/// the deployment will be deleted / cleaned up on the old server.\n///\n/// Note. This method updates only the fields which are set in the [_PartialDeploymentConfig],\n/// effectively merging diffs into the final document.\n/// This is helpful when multiple users are using\n/// the same resources concurrently by ensuring no unintentional\n/// field changes occur from out of date local state.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Deployment)]\n#[error(serror::Error)]\npub struct UpdateDeployment {\n  /// The deployment id to update.\n  pub id: String,\n  /// The partial config update.\n  pub config: _PartialDeploymentConfig,\n}\n\n//\n\n/// Rename the deployment at id to the given name. Response: [Update].\n///\n/// Note. If a container is created for the deployment, it will be renamed using\n/// `docker rename ...`.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RenameDeployment {\n  /// The id of the deployment to rename.\n  pub id: String,\n  /// The new name.\n  pub name: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/write/mod.rs",
    "content": "mod action;\nmod alerter;\nmod api_key;\nmod build;\nmod builder;\nmod deployment;\nmod permissions;\nmod procedure;\nmod provider;\nmod repo;\nmod resource;\nmod server;\nmod stack;\nmod sync;\nmod tags;\nmod user;\nmod user_group;\nmod variable;\n\npub use action::*;\npub use alerter::*;\npub use api_key::*;\npub use build::*;\npub use builder::*;\npub use deployment::*;\npub use permissions::*;\npub use procedure::*;\npub use provider::*;\npub use repo::*;\npub use resource::*;\npub use server::*;\npub use stack::*;\npub use sync::*;\npub use tags::*;\npub use user::*;\npub use user_group::*;\npub use variable::*;\n\npub trait KomodoWriteRequest: resolver_api::HasResponse {}\n"
  },
  {
    "path": "client/core/rs/src/api/write/permissions.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  NoData, ResourceTarget, ResourceTargetVariant,\n  permission::{PermissionLevelAndSpecifics, UserTarget},\n};\n\nuse super::KomodoWriteRequest;\n\n/// **Admin only.** Update a user or user groups permission on a resource.\n/// Response: [NoData].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdatePermissionOnTargetResponse)]\n#[error(serror::Error)]\npub struct UpdatePermissionOnTarget {\n  /// Specify the user or user group.\n  pub user_target: UserTarget,\n  /// Specify the target resource.\n  pub resource_target: ResourceTarget,\n  /// Specify the permission level.\n  pub permission: PermissionLevelAndSpecifics,\n}\n\n#[typeshare]\npub type UpdatePermissionOnTargetResponse = NoData;\n\n//\n\n/// **Admin only.** Update a user or user groups base permission level on a resource type.\n/// Response: [NoData].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdatePermissionOnResourceTypeResponse)]\n#[error(serror::Error)]\npub struct UpdatePermissionOnResourceType {\n  /// Specify the user or user group.\n  pub user_target: UserTarget,\n  /// The resource type: eg. Server, Build, Deployment, etc.\n  pub resource_type: ResourceTargetVariant,\n  /// The base permission level.\n  pub permission: PermissionLevelAndSpecifics,\n}\n\n#[typeshare]\npub type UpdatePermissionOnResourceTypeResponse = NoData;\n\n//\n\n/// **Admin only.** Update a user's \"base\" permissions, eg. \"enabled\".\n/// Response: [NoData].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateUserBasePermissionsResponse)]\n#[error(serror::Error)]\npub struct UpdateUserBasePermissions {\n  /// The target user.\n  pub user_id: String,\n  /// If specified, will update users enabled state.\n  pub enabled: Option<bool>,\n  /// If specified, will update user's ability to create servers.\n  pub create_servers: Option<bool>,\n  /// If specified, will update user's ability to create builds.\n  pub create_builds: Option<bool>,\n}\n\n#[typeshare]\npub type UpdateUserBasePermissionsResponse = NoData;\n\n/// **Super Admin only.** Update's whether a user is admin.\n/// Response: [NoData].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateUserAdminResponse)]\n#[error(serror::Error)]\npub struct UpdateUserAdmin {\n  /// The target user.\n  pub user_id: String,\n  /// Whether user should be admin.\n  pub admin: bool,\n}\n\n#[typeshare]\npub type UpdateUserAdminResponse = NoData;\n"
  },
  {
    "path": "client/core/rs/src/api/write/procedure.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  procedure::{_PartialProcedureConfig, Procedure},\n  update::Update,\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create a procedure. Response: [Procedure].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateProcedureResponse)]\n#[error(serror::Error)]\npub struct CreateProcedure {\n  /// The name given to newly created build.\n  pub name: String,\n  /// Optional partial config to initialize the procedure with.\n  #[serde(default)]\n  pub config: _PartialProcedureConfig,\n}\n\n#[typeshare]\npub type CreateProcedureResponse = Procedure;\n\n//\n\n/// Creates a new procedure with given `name` and the configuration\n/// of the procedure at the given `id`. Response: [Procedure].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CopyProcedureResponse)]\n#[error(serror::Error)]\npub struct CopyProcedure {\n  /// The name of the new procedure.\n  pub name: String,\n  /// The id of the procedure to copy.\n  pub id: String,\n}\n\n#[typeshare]\npub type CopyProcedureResponse = Procedure;\n\n//\n\n/// Deletes the procedure at the given id, and returns the deleted procedure.\n/// Response: [Procedure]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteProcedureResponse)]\n#[error(serror::Error)]\npub struct DeleteProcedure {\n  /// The id or name of the procedure to delete.\n  pub id: String,\n}\n\n#[typeshare]\npub type DeleteProcedureResponse = Procedure;\n\n//\n\n/// Update the procedure at the given id, and return the updated procedure.\n/// Response: [Procedure].\n///\n/// Note. This method updates only the fields which are set in the [_PartialProcedureConfig],\n/// effectively merging diffs into the final document.\n/// This is helpful when multiple users are using\n/// the same resources concurrently by ensuring no unintentional\n/// field changes occur from out of date local state.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateProcedureResponse)]\n#[error(serror::Error)]\npub struct UpdateProcedure {\n  /// The id of the procedure to update.\n  pub id: String,\n  /// The partial config update.\n  pub config: _PartialProcedureConfig,\n}\n\n#[typeshare]\npub type UpdateProcedureResponse = Procedure;\n\n//\n\n/// Rename the Procedure at id to the given name.\n/// Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RenameProcedure {\n  /// The id or name of the Procedure to rename.\n  pub id: String,\n  /// The new name.\n  pub name: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/write/provider.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::provider::*;\n\nuse super::KomodoWriteRequest;\n\n/// **Admin only.** Create a git provider account.\n/// Response: [GitProviderAccount].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateGitProviderAccountResponse)]\n#[error(serror::Error)]\npub struct CreateGitProviderAccount {\n  /// The initial account config. Anything in the _id field will be ignored,\n  /// as this is generated on creation.\n  pub account: _PartialGitProviderAccount,\n}\n\n#[typeshare]\npub type CreateGitProviderAccountResponse = GitProviderAccount;\n\n//\n\n/// **Admin only.** Update a git provider account.\n/// Response: [GitProviderAccount].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateGitProviderAccountResponse)]\n#[error(serror::Error)]\npub struct UpdateGitProviderAccount {\n  /// The id of the git provider account to update.\n  pub id: String,\n  /// The partial git provider account.\n  pub account: _PartialGitProviderAccount,\n}\n\n#[typeshare]\npub type UpdateGitProviderAccountResponse = GitProviderAccount;\n\n//\n\n/// **Admin only.** Delete a git provider account.\n/// Response: [DeleteGitProviderAccountResponse].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteGitProviderAccountResponse)]\n#[error(serror::Error)]\npub struct DeleteGitProviderAccount {\n  /// The id of the git provider to delete\n  pub id: String,\n}\n\n#[typeshare]\npub type DeleteGitProviderAccountResponse = GitProviderAccount;\n\n//\n\n/// **Admin only.** Create a docker registry account.\n/// Response: [DockerRegistryAccount].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateDockerRegistryAccountResponse)]\n#[error(serror::Error)]\npub struct CreateDockerRegistryAccount {\n  pub account: _PartialDockerRegistryAccount,\n}\n\n#[typeshare]\npub type CreateDockerRegistryAccountResponse = DockerRegistryAccount;\n\n//\n\n/// **Admin only.** Update a docker registry account.\n/// Response: [DockerRegistryAccount].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateDockerRegistryAccountResponse)]\n#[error(serror::Error)]\npub struct UpdateDockerRegistryAccount {\n  /// The id of the docker registry to update\n  pub id: String,\n  /// The partial docker registry account.\n  pub account: _PartialDockerRegistryAccount,\n}\n\n#[typeshare]\npub type UpdateDockerRegistryAccountResponse = DockerRegistryAccount;\n\n//\n\n/// **Admin only.** Delete a docker registry account.\n/// Response: [DockerRegistryAccount].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteDockerRegistryAccountResponse)]\n#[error(serror::Error)]\npub struct DeleteDockerRegistryAccount {\n  /// The id of the docker registry account to delete\n  pub id: String,\n}\n\n#[typeshare]\npub type DeleteDockerRegistryAccountResponse = DockerRegistryAccount;\n"
  },
  {
    "path": "client/core/rs/src/api/write/repo.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  NoData,\n  repo::{_PartialRepoConfig, Repo},\n  update::Update,\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create a repo. Response: [Repo].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Repo)]\n#[error(serror::Error)]\npub struct CreateRepo {\n  /// The name given to newly created repo.\n  pub name: String,\n  /// Optional partial config to initialize the repo with.\n  #[serde(default)]\n  pub config: _PartialRepoConfig,\n}\n\n//\n\n/// Creates a new repo with given `name` and the configuration\n/// of the repo at the given `id`. Response: [Repo].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Repo)]\n#[error(serror::Error)]\npub struct CopyRepo {\n  /// The name of the new repo.\n  pub name: String,\n  /// The id of the repo to copy.\n  pub id: String,\n}\n\n//\n\n/// Deletes the repo at the given id, and returns the deleted repo.\n/// Response: [Repo]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Repo)]\n#[error(serror::Error)]\npub struct DeleteRepo {\n  /// The id or name of the repo to delete.\n  pub id: String,\n}\n\n//\n\n/// Update the repo at the given id, and return the updated repo.\n/// Response: [Repo].\n///\n/// Note. If the attached server for the repo changes,\n/// the repo will be deleted / cleaned up on the old server.\n///\n/// Note. This method updates only the fields which are set in the [_PartialRepoConfig],\n/// effectively merging diffs into the final document.\n/// This is helpful when multiple users are using\n/// the same resources concurrently by ensuring no unintentional\n/// field changes occur from out of date local state.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Repo)]\n#[error(serror::Error)]\npub struct UpdateRepo {\n  /// The id of the repo to update.\n  pub id: String,\n  /// The partial config update to apply.\n  pub config: _PartialRepoConfig,\n}\n\n//\n\n/// Rename the Repo at id to the given name.\n/// Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RenameRepo {\n  /// The id or name of the Repo to rename.\n  pub id: String,\n  /// The new name.\n  pub name: String,\n}\n\n//\n\n/// Trigger a refresh of the cached latest hash and message.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(NoData)]\n#[error(serror::Error)]\npub struct RefreshRepoCache {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub repo: String,\n}\n\n//\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum RepoWebhookAction {\n  Clone,\n  Pull,\n  Build,\n}\n\n/// Create a webhook on the github repo attached to the (Komodo) Repo resource.\n/// passed in request. Response: [CreateRepoWebhookResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateRepoWebhookResponse)]\n#[error(serror::Error)]\npub struct CreateRepoWebhook {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub repo: String,\n  /// \"Clone\" or \"Pull\" or \"Build\"\n  pub action: RepoWebhookAction,\n}\n\n#[typeshare]\npub type CreateRepoWebhookResponse = NoData;\n\n//\n\n/// Delete the webhook on the github repo attached to the (Komodo) Repo resource.\n/// passed in request. Response: [DeleteRepoWebhookResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteRepoWebhookResponse)]\n#[error(serror::Error)]\npub struct DeleteRepoWebhook {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub repo: String,\n  /// \"Clone\" or \"Pull\" or \"Build\"\n  pub action: RepoWebhookAction,\n}\n\n#[typeshare]\npub type DeleteRepoWebhookResponse = NoData;\n"
  },
  {
    "path": "client/core/rs/src/api/write/resource.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{NoData, ResourceTarget};\n\nuse super::KomodoWriteRequest;\n\n/// Update a resources common meta fields.\n/// - description\n/// - template\n/// - tags\n/// Response: [NoData].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateResourceMetaResponse)]\n#[error(serror::Error)]\npub struct UpdateResourceMeta {\n  /// The target resource to set update meta.\n  pub target: ResourceTarget,\n  /// New description to set,\n  /// or null for no update\n  pub description: Option<String>,\n  /// New template value (true or false),\n  /// or null for no update\n  pub template: Option<bool>,\n  /// The exact tags to set,\n  /// or null for no update\n  pub tags: Option<Vec<String>>,\n}\n\n#[typeshare]\npub type UpdateResourceMetaResponse = NoData;\n"
  },
  {
    "path": "client/core/rs/src/api/write/server.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  NoData,\n  server::{_PartialServerConfig, Server},\n  update::Update,\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create a server. Response: [Server].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Server)]\n#[error(serror::Error)]\npub struct CreateServer {\n  /// The name given to newly created server.\n  pub name: String,\n  /// Optional partial config to initialize the server with.\n  #[serde(default)]\n  pub config: _PartialServerConfig,\n}\n\n//\n\n/// Creates a new server with given `name` and the configuration\n/// of the server at the given `id`. Response: [Server].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Server)]\n#[error(serror::Error)]\npub struct CopyServer {\n  /// The name of the new server.\n  pub name: String,\n  /// The id of the server to copy.\n  pub id: String,\n}\n\n//\n\n/// Deletes the server at the given id, and returns the deleted server.\n/// Response: [Server]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Server)]\n#[error(serror::Error)]\npub struct DeleteServer {\n  /// The id or name of the server to delete.\n  pub id: String,\n}\n\n//\n\n/// Update the server at the given id, and return the updated server.\n/// Response: [Server].\n///\n/// Note. This method updates only the fields which are set in the [_PartialServerConfig],\n/// effectively merging diffs into the final document.\n/// This is helpful when multiple users are using\n/// the same resources concurrently by ensuring no unintentional\n/// field changes occur from out of date local state.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Server)]\n#[error(serror::Error)]\npub struct UpdateServer {\n  /// The id or name of the server to update.\n  pub id: String,\n  /// The partial config update to apply.\n  pub config: _PartialServerConfig,\n}\n\n//\n\n/// Rename an Server to the given name.\n/// Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RenameServer {\n  /// The id or name of the Server to rename.\n  pub id: String,\n  /// The new name.\n  pub name: String,\n}\n\n//\n\n/// Create a docker network on the server.\n/// Response: [Update]\n///\n/// `docker network create {name}`\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct CreateNetwork {\n  /// Server Id or name\n  pub server: String,\n  /// The name of the network to create.\n  pub name: String,\n}\n\n//\n\n/// Configures the behavior of [CreateTerminal] if the\n/// specified terminal name already exists.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]\npub enum TerminalRecreateMode {\n  /// Never kill the old terminal if it already exists.\n  /// If the command is different, returns error.\n  #[default]\n  Never,\n  /// Always kill the old terminal and create new one\n  Always,\n  /// Only kill and recreate if the command is different.\n  DifferentCommand,\n}\n\n/// Create a terminal on the server.\n/// Response: [NoData]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(NoData)]\n#[error(serror::Error)]\npub struct CreateTerminal {\n  /// Server Id or name\n  pub server: String,\n  /// The name of the terminal on the server to create.\n  pub name: String,\n  /// The shell command (eg `bash`) to init the shell.\n  ///\n  /// This can also include args:\n  /// `docker exec -it container sh`\n  ///\n  /// Default: `bash`\n  #[serde(default = \"default_command\")]\n  pub command: String,\n  /// Default: `Never`\n  #[serde(default)]\n  pub recreate: TerminalRecreateMode,\n}\n\nfn default_command() -> String {\n  String::from(\"bash\")\n}\n\n//\n\n/// Delete a terminal on the server.\n/// Response: [NoData]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(NoData)]\n#[error(serror::Error)]\npub struct DeleteTerminal {\n  /// Server Id or name\n  pub server: String,\n  /// The name of the terminal on the server to delete.\n  pub terminal: String,\n}\n\n/// Delete all terminals on the server.\n/// Response: [NoData]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(NoData)]\n#[error(serror::Error)]\npub struct DeleteAllTerminals {\n  /// Server Id or name\n  pub server: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/write/stack.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  NoData,\n  stack::{_PartialStackConfig, Stack},\n  update::Update,\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create a stack. Response: [Stack].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Stack)]\n#[error(serror::Error)]\npub struct CreateStack {\n  /// The name given to newly created stack.\n  pub name: String,\n  /// Optional partial config to initialize the stack with.\n  #[serde(default)]\n  pub config: _PartialStackConfig,\n}\n\n//\n\n/// Creates a new stack with given `name` and the configuration\n/// of the stack at the given `id`. Response: [Stack].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Stack)]\n#[error(serror::Error)]\npub struct CopyStack {\n  /// The name of the new stack.\n  pub name: String,\n  /// The id of the stack to copy.\n  pub id: String,\n}\n\n//\n\n/// Deletes the stack at the given id, and returns the deleted stack.\n/// Response: [Stack]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Stack)]\n#[error(serror::Error)]\npub struct DeleteStack {\n  /// The id or name of the stack to delete.\n  pub id: String,\n}\n\n//\n\n/// Update the stack at the given id, and return the updated stack.\n/// Response: [Stack].\n///\n/// Note. If the attached server for the stack changes,\n/// the stack will be deleted / cleaned up on the old server.\n///\n/// Note. This method updates only the fields which are set in the [_PartialStackConfig],\n/// merging diffs into the final document.\n/// This is helpful when multiple users are using\n/// the same resources concurrently by ensuring no unintentional\n/// field changes occur from out of date local state.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Stack)]\n#[error(serror::Error)]\npub struct UpdateStack {\n  /// The id of the Stack to update.\n  pub id: String,\n  /// The partial config update to apply.\n  pub config: _PartialStackConfig,\n}\n\n//\n\n/// Rename the stack at id to the given name. Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RenameStack {\n  /// The id of the stack to rename.\n  pub id: String,\n  /// The new name.\n  pub name: String,\n}\n\n//\n\n/// Update file contents in Files on Server or Git Repo mode. Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct WriteStackFileContents {\n  /// The name or id of the target Stack.\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n  /// The file path relative to the stack run directory,\n  /// or absolute path.\n  pub file_path: String,\n  /// The contents to write.\n  pub contents: String,\n}\n\n//\n\n/// Trigger a refresh of the cached compose file contents.\n/// Refreshes:\n///   - Whether the remote file is missing\n///   - The latest json, and for repos, the remote contents, hash, and message.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(NoData)]\n#[error(serror::Error)]\npub struct RefreshStackCache {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n}\n\n//\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum StackWebhookAction {\n  Refresh,\n  Deploy,\n}\n\n/// Create a webhook on the github repo attached to the stack\n/// passed in request. Response: [CreateStackWebhookResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateStackWebhookResponse)]\n#[error(serror::Error)]\npub struct CreateStackWebhook {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n  /// \"Refresh\" or \"Deploy\"\n  pub action: StackWebhookAction,\n}\n\n#[typeshare]\npub type CreateStackWebhookResponse = NoData;\n\n//\n\n/// Delete the webhook on the github repo attached to the stack\n/// passed in request. Response: [DeleteStackWebhookResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteStackWebhookResponse)]\n#[error(serror::Error)]\npub struct DeleteStackWebhook {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub stack: String,\n  /// \"Refresh\" or \"Deploy\"\n  pub action: StackWebhookAction,\n}\n\n#[typeshare]\npub type DeleteStackWebhookResponse = NoData;\n"
  },
  {
    "path": "client/core/rs/src/api/write/sync.rs",
    "content": "use clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  NoData,\n  sync::{_PartialResourceSyncConfig, ResourceSync},\n  update::Update,\n};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create a sync. Response: [ResourceSync].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(ResourceSync)]\n#[error(serror::Error)]\npub struct CreateResourceSync {\n  /// The name given to newly created sync.\n  pub name: String,\n  /// Optional partial config to initialize the sync with.\n  #[serde(default)]\n  pub config: _PartialResourceSyncConfig,\n}\n\n//\n\n/// Creates a new sync with given `name` and the configuration\n/// of the sync at the given `id`. Response: [ResourceSync].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(ResourceSync)]\n#[error(serror::Error)]\npub struct CopyResourceSync {\n  /// The name of the new sync.\n  pub name: String,\n  /// The id of the sync to copy.\n  pub id: String,\n}\n\n//\n\n/// Deletes the sync at the given id, and returns the deleted sync.\n/// Response: [ResourceSync]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(ResourceSync)]\n#[error(serror::Error)]\npub struct DeleteResourceSync {\n  /// The id or name of the sync to delete.\n  pub id: String,\n}\n\n//\n\n/// Update the sync at the given id, and return the updated sync.\n/// Response: [ResourceSync].\n///\n/// Note. This method updates only the fields which are set in the [_PartialResourceSyncConfig],\n/// effectively merging diffs into the final document.\n/// This is helpful when multiple users are using\n/// the same resources concurrently by ensuring no unintentional\n/// field changes occur from out of date local state.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(ResourceSync)]\n#[error(serror::Error)]\npub struct UpdateResourceSync {\n  /// The id of the sync to update.\n  pub id: String,\n  /// The partial config update to apply.\n  pub config: _PartialResourceSyncConfig,\n}\n\n//\n\n/// Rename the ResourceSync at id to the given name.\n/// Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct RenameResourceSync {\n  /// The id or name of the ResourceSync to rename.\n  pub id: String,\n  /// The new name.\n  pub name: String,\n}\n\n//\n\n/// Trigger a refresh of the computed diff logs for view. Response: [ResourceSync]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(ResourceSync)]\n#[error(serror::Error)]\npub struct RefreshResourceSyncPending {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub sync: String,\n}\n\n//\n\n/// Rename the stack at id to the given name. Response: [Update].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct WriteSyncFileContents {\n  /// The name or id of the target Sync.\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub sync: String,\n  /// If this file was under a resource folder, this will be the folder.\n  /// Otherwise, it should be empty string.\n  pub resource_path: String,\n  /// The file path relative to the resource path.\n  pub file_path: String,\n  /// The contents to write.\n  pub contents: String,\n}\n\n//\n\n/// Exports matching resources, and writes to the target sync's resource file. Response: [Update]\n///\n/// Note. Will fail if the Sync is not `managed`.\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Resolve,\n  EmptyTraits,\n  Parser,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Update)]\n#[error(serror::Error)]\npub struct CommitSync {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub sync: String,\n}\n\n//\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum SyncWebhookAction {\n  Refresh,\n  Sync,\n}\n\n/// Create a webhook on the github repo attached to the sync\n/// passed in request. Response: [CreateSyncWebhookResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateSyncWebhookResponse)]\n#[error(serror::Error)]\npub struct CreateSyncWebhook {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub sync: String,\n  /// \"Refresh\" or \"Sync\"\n  pub action: SyncWebhookAction,\n}\n\n#[typeshare]\npub type CreateSyncWebhookResponse = NoData;\n\n//\n\n/// Delete the webhook on the github repo attached to the sync\n/// passed in request. Response: [DeleteSyncWebhookResponse]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteSyncWebhookResponse)]\n#[error(serror::Error)]\npub struct DeleteSyncWebhook {\n  /// Id or name\n  #[serde(alias = \"id\", alias = \"name\")]\n  pub sync: String,\n  /// \"Refresh\" or \"Sync\"\n  pub action: SyncWebhookAction,\n}\n\n#[typeshare]\npub type DeleteSyncWebhookResponse = NoData;\n"
  },
  {
    "path": "client/core/rs/src/api/write/tags.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::tag::{Tag, TagColor};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// Create a tag. Response: [Tag].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Tag)]\n#[error(serror::Error)]\npub struct CreateTag {\n  /// The name of the tag.\n  pub name: String,\n  /// Tag color. Default: Slate.\n  pub color: Option<TagColor>,\n}\n\n//\n\n/// Delete a tag, and return the deleted tag. Response: [Tag].\n///\n/// Note. Will also remove this tag from all attached resources.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Tag)]\n#[error(serror::Error)]\npub struct DeleteTag {\n  /// The id of the tag to delete.\n  pub id: String,\n}\n\n//\n\n/// Rename a tag at id. Response: [Tag].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Tag)]\n#[error(serror::Error)]\npub struct RenameTag {\n  /// The id of the tag to rename.\n  pub id: String,\n  /// The new name of the tag.\n  pub name: String,\n}\n\n/// Update color for tag. Response: [Tag].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(Tag)]\n#[error(serror::Error)]\npub struct UpdateTagColor {\n  /// The name or id of the tag to update.\n  pub tag: String,\n  /// The new color for the tag.\n  pub color: TagColor,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/write/user.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{NoData, user::User};\n\nuse super::KomodoWriteRequest;\n\n//\n\n/// **Only for local users**. Update the calling users username.\n/// Response: [NoData].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateUserUsernameResponse)]\n#[error(serror::Error)]\npub struct UpdateUserUsername {\n  pub username: String,\n}\n\n#[typeshare]\npub type UpdateUserUsernameResponse = NoData;\n\n//\n\n/// **Only for local users**. Update the calling users password.\n/// Response: [NoData].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateUserPasswordResponse)]\n#[error(serror::Error)]\npub struct UpdateUserPassword {\n  pub password: String,\n}\n\n#[typeshare]\npub type UpdateUserPasswordResponse = NoData;\n\n//\n\n/// **Admin only**. Delete a user.\n/// Admins can delete any non-admin user.\n/// Only Super Admin can delete an admin.\n/// No users can delete a Super Admin user.\n/// User cannot delete themselves.\n/// Response: [NoData].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteUserResponse)]\n#[error(serror::Error)]\npub struct DeleteUser {\n  /// User id or username\n  #[serde(alias = \"username\", alias = \"id\")]\n  pub user: String,\n}\n\n#[typeshare]\npub type DeleteUserResponse = User;\n\n//\n\n/// **Admin only.** Create a local user.\n/// Response: [User].\n///\n/// Note. Not to be confused with /auth/SignUpLocalUser.\n/// This method requires admin user credentials, and can\n/// bypass disabled user registration.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateLocalUserResponse)]\n#[error(serror::Error)]\npub struct CreateLocalUser {\n  /// The username for the local user.\n  pub username: String,\n  /// A password for the local user.\n  pub password: String,\n}\n\n#[typeshare]\npub type CreateLocalUserResponse = User;\n\n//\n\n/// **Admin only.** Create a service user.\n/// Response: [User].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateServiceUserResponse)]\n#[error(serror::Error)]\npub struct CreateServiceUser {\n  /// The username for the service user.\n  pub username: String,\n  /// A description for the service user.\n  pub description: String,\n}\n\n#[typeshare]\npub type CreateServiceUserResponse = User;\n\n//\n\n/// **Admin only.** Update a service user's description.\n/// Response: [User].\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateServiceUserDescriptionResponse)]\n#[error(serror::Error)]\npub struct UpdateServiceUserDescription {\n  /// The service user's username\n  pub username: String,\n  /// A new description for the service user.\n  pub description: String,\n}\n\n#[typeshare]\npub type UpdateServiceUserDescriptionResponse = User;\n"
  },
  {
    "path": "client/core/rs/src/api/write/user_group.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::user_group::UserGroup;\n\nuse super::KomodoWriteRequest;\n\n/// **Admin only.** Create a user group. Response: [UserGroup]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UserGroup)]\n#[error(serror::Error)]\npub struct CreateUserGroup {\n  /// The name to assign to the new UserGroup\n  pub name: String,\n}\n\n//\n\n/// **Admin only.** Rename a user group. Response: [UserGroup]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UserGroup)]\n#[error(serror::Error)]\npub struct RenameUserGroup {\n  /// The id of the UserGroup\n  pub id: String,\n  /// The new name for the UserGroup\n  pub name: String,\n}\n\n//\n\n/// **Admin only.** Delete a user group. Response: [UserGroup]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UserGroup)]\n#[error(serror::Error)]\npub struct DeleteUserGroup {\n  /// The id of the UserGroup\n  pub id: String,\n}\n\n//\n\n/// **Admin only.** Add a user to a user group. Response: [UserGroup]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UserGroup)]\n#[error(serror::Error)]\npub struct AddUserToUserGroup {\n  /// The name or id of UserGroup that user should be added to.\n  pub user_group: String,\n  /// The id or username of the user to add\n  pub user: String,\n}\n\n//\n\n/// **Admin only.** Remove a user from a user group. Response: [UserGroup]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UserGroup)]\n#[error(serror::Error)]\npub struct RemoveUserFromUserGroup {\n  /// The name or id of UserGroup that user should be removed from.\n  pub user_group: String,\n  /// The id or username of the user to remove\n  pub user: String,\n}\n\n//\n\n/// **Admin only.** Completely override the users in the group.\n/// Response: [UserGroup]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UserGroup)]\n#[error(serror::Error)]\npub struct SetUsersInUserGroup {\n  /// Id or name.\n  pub user_group: String,\n  /// The user ids or usernames to hard set as the group's users.\n  pub users: Vec<String>,\n}\n\n//\n\n/// **Admin only.** Set `everyone` property of User Group.\n/// Response: [UserGroup]\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UserGroup)]\n#[error(serror::Error)]\npub struct SetEveryoneUserGroup {\n  /// Id or name.\n  pub user_group: String,\n  /// Whether this user group applies to everyone.\n  pub everyone: bool,\n}\n"
  },
  {
    "path": "client/core/rs/src/api/write/variable.rs",
    "content": "use derive_empty_traits::EmptyTraits;\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::variable::Variable;\n\nuse super::KomodoWriteRequest;\n\n/// **Admin only.** Create variable. Response: [Variable].\n#[typeshare]\n#[derive(\n  Debug, Clone, Serialize, Deserialize, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(CreateVariableResponse)]\n#[error(serror::Error)]\npub struct CreateVariable {\n  /// The name of the variable to create.\n  pub name: String,\n  /// The initial value of the variable. defualt: \"\".\n  #[serde(default)]\n  pub value: String,\n  /// The initial value of the description. default: \"\".\n  #[serde(default)]\n  pub description: String,\n  /// Whether to make this a secret variable.\n  #[serde(default)]\n  pub is_secret: bool,\n}\n\n#[typeshare]\npub type CreateVariableResponse = Variable;\n\n//\n\n/// **Admin only.** Update variable value. Response: [Variable].\n#[typeshare]\n#[derive(\n  Debug, Clone, Serialize, Deserialize, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateVariableValueResponse)]\n#[error(serror::Error)]\npub struct UpdateVariableValue {\n  /// The name of the variable to update.\n  pub name: String,\n  /// The value to set.\n  pub value: String,\n}\n\n#[typeshare]\npub type UpdateVariableValueResponse = Variable;\n\n//\n\n/// **Admin only.** Update variable description. Response: [Variable].\n#[typeshare]\n#[derive(\n  Debug, Clone, Serialize, Deserialize, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateVariableDescriptionResponse)]\n#[error(serror::Error)]\npub struct UpdateVariableDescription {\n  /// The name of the variable to update.\n  pub name: String,\n  /// The description to set.\n  pub description: String,\n}\n\n#[typeshare]\npub type UpdateVariableDescriptionResponse = Variable;\n\n//\n\n/// **Admin only.** Update whether variable is secret. Response: [Variable].\n#[typeshare]\n#[derive(\n  Debug, Clone, Serialize, Deserialize, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(UpdateVariableIsSecretResponse)]\n#[error(serror::Error)]\npub struct UpdateVariableIsSecret {\n  /// The name of the variable to update.\n  pub name: String,\n  /// Whether variable is secret.\n  pub is_secret: bool,\n}\n\n#[typeshare]\npub type UpdateVariableIsSecretResponse = Variable;\n\n//\n\n/// **Admin only.** Delete a variable. Response: [Variable].\n#[typeshare]\n#[derive(\n  Debug, Clone, Serialize, Deserialize, Resolve, EmptyTraits,\n)]\n#[empty_traits(KomodoWriteRequest)]\n#[response(DeleteVariableResponse)]\n#[error(serror::Error)]\npub struct DeleteVariable {\n  pub name: String,\n}\n\n#[typeshare]\npub type DeleteVariableResponse = Variable;\n"
  },
  {
    "path": "client/core/rs/src/busy.rs",
    "content": "use crate::entities::{\n  action::ActionActionState, build::BuildActionState,\n  deployment::DeploymentActionState, procedure::ProcedureActionState,\n  repo::RepoActionState, server::ServerActionState,\n  stack::StackActionState, sync::ResourceSyncActionState,\n};\n\npub trait Busy {\n  fn busy(&self) -> bool;\n}\n\nimpl Busy for ServerActionState {\n  fn busy(&self) -> bool {\n    self.pruning_containers\n      || self.pruning_images\n      || self.pruning_networks\n      || self.pruning_volumes\n      || self.starting_containers\n      || self.restarting_containers\n      || self.pausing_containers\n      || self.unpausing_containers\n      || self.stopping_containers\n  }\n}\n\nimpl Busy for DeploymentActionState {\n  fn busy(&self) -> bool {\n    self.deploying\n      || self.starting\n      || self.restarting\n      || self.pausing\n      || self.unpausing\n      || self.stopping\n      || self.destroying\n      || self.renaming\n  }\n}\n\nimpl Busy for StackActionState {\n  fn busy(&self) -> bool {\n    self.deploying\n      || self.starting\n      || self.restarting\n      || self.pausing\n      || self.unpausing\n      || self.stopping\n      || self.destroying\n  }\n}\n\nimpl Busy for BuildActionState {\n  fn busy(&self) -> bool {\n    self.building\n  }\n}\n\nimpl Busy for RepoActionState {\n  fn busy(&self) -> bool {\n    self.cloning || self.pulling || self.building\n  }\n}\n\nimpl Busy for ProcedureActionState {\n  fn busy(&self) -> bool {\n    self.running\n  }\n}\n\nimpl Busy for ActionActionState {\n  fn busy(&self) -> bool {\n    self.running > 0\n  }\n}\n\nimpl Busy for ResourceSyncActionState {\n  fn busy(&self) -> bool {\n    self.syncing\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/deserializers/conversion.rs",
    "content": "use serde::{\n  Deserialize, Deserializer,\n  de::{Visitor, value::SeqAccessDeserializer},\n};\n\nuse crate::entities::deployment::Conversion;\n\npub fn conversions_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<String, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(ConversionVisitor)\n}\n\npub fn option_conversions_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<Option<String>, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(OptionConversionVisitor)\n}\n\nstruct ConversionVisitor;\n\nimpl<'de> Visitor<'de> for ConversionVisitor {\n  type Value = String;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"string or Vec<Conversion>\")\n  }\n\n  fn visit_string<E>(self, out: String) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    if out.is_empty() || out.ends_with('\\n') {\n      Ok(out)\n    } else {\n      Ok(out + \"\\n\")\n    }\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Self::visit_string(self, v.to_string())\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    let res = Vec::<Conversion>::deserialize(\n      SeqAccessDeserializer::new(seq),\n    )?;\n    let res = res\n      .iter()\n      .map(|Conversion { local, container }| {\n        format!(\"  {local}: {container}\")\n      })\n      .collect::<Vec<_>>()\n      .join(\"\\n\");\n    let extra = if res.is_empty() { \"\" } else { \"\\n\" };\n    Ok(res + extra)\n  }\n}\n\nstruct OptionConversionVisitor;\n\nimpl<'de> Visitor<'de> for OptionConversionVisitor {\n  type Value = Option<String>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"null or string or Vec<Conversion>\")\n  }\n\n  fn visit_string<E>(self, v: String) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    ConversionVisitor.visit_string(v).map(Some)\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    ConversionVisitor.visit_str(v).map(Some)\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    ConversionVisitor.visit_seq(seq).map(Some)\n  }\n\n  fn visit_none<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n\n  fn visit_unit<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/deserializers/environment.rs",
    "content": "use serde::{\n  Deserialize, Deserializer,\n  de::{Visitor, value::SeqAccessDeserializer},\n};\n\nuse crate::entities::EnvironmentVar;\n\npub fn env_vars_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<String, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(EnvironmentVarVisitor)\n}\n\npub fn option_env_vars_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<Option<String>, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(OptionEnvVarVisitor)\n}\n\nstruct EnvironmentVarVisitor;\n\nimpl<'de> Visitor<'de> for EnvironmentVarVisitor {\n  type Value = String;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"string or Vec<EnvironmentVar>\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    let out = v.to_string();\n    if out.is_empty() || out.ends_with('\\n') {\n      Ok(out)\n    } else {\n      Ok(out + \"\\n\")\n    }\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    let vars = Vec::<EnvironmentVar>::deserialize(\n      SeqAccessDeserializer::new(seq),\n    )?;\n    let vars = vars\n      .iter()\n      .map(|EnvironmentVar { variable, value }| {\n        format!(\"  {variable} = {value}\")\n      })\n      .collect::<Vec<_>>()\n      .join(\"\\n\");\n    let extra = if vars.is_empty() { \"\" } else { \"\\n\" };\n    Ok(vars + extra)\n  }\n}\n\nstruct OptionEnvVarVisitor;\n\nimpl<'de> Visitor<'de> for OptionEnvVarVisitor {\n  type Value = Option<String>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"null or string or Vec<EnvironmentVar>\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    EnvironmentVarVisitor.visit_str(v).map(Some)\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    EnvironmentVarVisitor.visit_seq(seq).map(Some)\n  }\n\n  fn visit_none<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n\n  fn visit_unit<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/deserializers/file_contents.rs",
    "content": "use serde::{Deserializer, de::Visitor};\n\n/// Using this ensures the file contents end with trailing '\\n'\npub fn file_contents_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<String, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(FileContentsVisitor)\n}\n\n/// Using this ensures the file contents end with trailing '\\n'\npub fn option_file_contents_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<Option<String>, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(OptionFileContentsVisitor)\n}\n\nstruct FileContentsVisitor;\n\nimpl Visitor<'_> for FileContentsVisitor {\n  type Value = String;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"string\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    let out = v.trim_end().to_string();\n    if out.is_empty() {\n      Ok(out)\n    } else {\n      Ok(out + \"\\n\")\n    }\n  }\n}\n\nstruct OptionFileContentsVisitor;\n\nimpl Visitor<'_> for OptionFileContentsVisitor {\n  type Value = Option<String>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"null or string\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    FileContentsVisitor.visit_str(v).map(Some)\n  }\n\n  fn visit_none<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n\n  fn visit_unit<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/deserializers/forgiving_vec.rs",
    "content": "use serde::{\n  Deserialize, Deserializer,\n  de::{IntoDeserializer, Visitor},\n};\n\n#[derive(Debug, Clone)]\npub struct ForgivingVec<T>(pub Vec<T>);\n\nimpl<T> ForgivingVec<T> {\n  pub fn iter(&self) -> std::slice::Iter<'_, T> {\n    self.0.iter()\n  }\n\n  pub fn is_empty(&self) -> bool {\n    self.0.is_empty()\n  }\n}\n\nimpl<T> Default for ForgivingVec<T> {\n  fn default() -> Self {\n    ForgivingVec(Vec::new())\n  }\n}\n\nimpl<T> IntoIterator for ForgivingVec<T> {\n  type Item = T;\n  type IntoIter = <Vec<T> as IntoIterator>::IntoIter;\n  fn into_iter(self) -> Self::IntoIter {\n    self.0.into_iter()\n  }\n}\n\nimpl<T> FromIterator<T> for ForgivingVec<T> {\n  fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {\n    Self(Vec::from_iter(iter))\n  }\n}\n\nimpl<'de, T: Deserialize<'de>> Deserialize<'de> for ForgivingVec<T> {\n  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n  where\n    D: Deserializer<'de>,\n  {\n    deserializer.deserialize_seq(ForgivingVecVisitor::<T>(\n      std::marker::PhantomData,\n    ))\n  }\n}\n\nstruct ForgivingVecVisitor<T>(std::marker::PhantomData<T>);\n\nimpl<'de, T: Deserialize<'de>> Visitor<'de>\n  for ForgivingVecVisitor<T>\n{\n  type Value = ForgivingVec<T>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"Vec<T>\")\n  }\n\n  fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>\n  where\n    S: serde::de::SeqAccess<'de>,\n  {\n    let mut res =\n      Vec::with_capacity(seq.size_hint().unwrap_or_default());\n    loop {\n      match seq.next_element::<serde_json::Value>() {\n        Ok(Some(value)) => {\n          match T::deserialize(value.clone().into_deserializer()) {\n            Ok(item) => res.push(item),\n            Err(e) => {\n              // Since this is used to parse startup config (including logging config),\n              // the tracing logging is not initialized. Need to use eprintln.\n              eprintln!(\n                \"WARN: failed to parse item in list | {value:?} | {e:?}\",\n              )\n            }\n          }\n        }\n        Ok(None) => break,\n        Err(e) => {\n          eprintln!(\"WARN: failed to get item in list | {e:?}\");\n        }\n      }\n    }\n    Ok(ForgivingVec(res))\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/deserializers/item_or_vec.rs",
    "content": "//! # Item or Vec<Item> deserializer.\n//!\n//! Used to convert `item: T` (struct / map) -> `item: Vec<T>` (seq) in schemas with backward compatibility.\n//! Supports deserializing either a T as Vec<T> with length 1, or a seq as Vec<T> directly.\n\nuse serde::{\n  Deserialize, Deserializer,\n  de::{\n    DeserializeOwned, IntoDeserializer, Visitor,\n    value::{MapAccessDeserializer, SeqAccessDeserializer},\n  },\n};\n\npub fn item_or_vec_deserializer<'de, D, T>(\n  deserializer: D,\n) -> Result<Vec<T>, D::Error>\nwhere\n  D: Deserializer<'de>,\n  T: DeserializeOwned,\n{\n  deserializer\n    .deserialize_any(ItemOrVecVisitor::<T>(std::marker::PhantomData))\n}\n\npub fn option_item_or_vec_deserializer<'de, D, T>(\n  deserializer: D,\n) -> Result<Option<Vec<T>>, D::Error>\nwhere\n  D: Deserializer<'de>,\n  T: DeserializeOwned,\n{\n  deserializer.deserialize_any(OptionItemOrVecVisitor::<T>(\n    std::marker::PhantomData,\n  ))\n}\n\nstruct ItemOrVecVisitor<T>(std::marker::PhantomData<T>);\n\nimpl<'de, T> Visitor<'de> for ItemOrVecVisitor<T>\nwhere\n  T: Deserialize<'de>,\n{\n  type Value = Vec<T>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"Item or Vec<Item>\")\n  }\n\n  fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::MapAccess<'de>,\n  {\n    T::deserialize(\n      MapAccessDeserializer::new(map).into_deserializer(),\n    )\n    .map(|r| vec![r])\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    Vec::<T>::deserialize(\n      SeqAccessDeserializer::new(seq).into_deserializer(),\n    )\n  }\n}\n\nstruct OptionItemOrVecVisitor<T>(std::marker::PhantomData<T>);\n\nimpl<'de, T> Visitor<'de> for OptionItemOrVecVisitor<T>\nwhere\n  T: Deserialize<'de>,\n{\n  type Value = Option<Vec<T>>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"null or Item or Vec<Item>\")\n  }\n\n  fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::MapAccess<'de>,\n  {\n    ItemOrVecVisitor::<T>(std::marker::PhantomData)\n      .visit_map(map)\n      .map(Some)\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    ItemOrVecVisitor::<T>(std::marker::PhantomData)\n      .visit_seq(seq)\n      .map(Some)\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/deserializers/labels.rs",
    "content": "use serde::{\n  Deserialize, Deserializer,\n  de::{Visitor, value::SeqAccessDeserializer},\n};\n\nuse crate::entities::EnvironmentVar;\n\npub fn labels_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<String, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(LabelVisitor)\n}\n\npub fn option_labels_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<Option<String>, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(OptionLabelVisitor)\n}\n\nstruct LabelVisitor;\n\nimpl<'de> Visitor<'de> for LabelVisitor {\n  type Value = String;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"string or Vec<EnvironmentVar>\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    let out = v.to_string();\n    if out.is_empty() || out.ends_with('\\n') {\n      Ok(out)\n    } else {\n      Ok(out + \"\\n\")\n    }\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    let vars = Vec::<EnvironmentVar>::deserialize(\n      SeqAccessDeserializer::new(seq),\n    )?;\n    let vars = vars\n      .iter()\n      .map(|EnvironmentVar { variable, value }| {\n        format!(\"  {variable}: {value}\")\n      })\n      .collect::<Vec<_>>()\n      .join(\"\\n\");\n    let extra = if vars.is_empty() { \"\" } else { \"\\n\" };\n    Ok(vars + extra)\n  }\n}\n\nstruct OptionLabelVisitor;\n\nimpl<'de> Visitor<'de> for OptionLabelVisitor {\n  type Value = Option<String>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"null or string or Vec<EnvironmentVar>\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    LabelVisitor.visit_str(v).map(Some)\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    LabelVisitor.visit_seq(seq).map(Some)\n  }\n\n  fn visit_none<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n\n  fn visit_unit<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/deserializers/maybe_string_i64.rs",
    "content": "use serde::{Deserializer, de::Visitor};\n\npub fn maybe_string_i64_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<i64, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(MaybeStringI64Visitor)\n}\n\npub fn option_maybe_string_i64_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<Option<i64>, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(OptionMaybeStringI64Visitor)\n}\n\nstruct MaybeStringI64Visitor;\n\nimpl Visitor<'_> for MaybeStringI64Visitor {\n  type Value = i64;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"number or string number\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    v.parse::<i64>().map_err(E::custom)\n  }\n\n  fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(v as i64)\n  }\n\n  fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(v as i64)\n  }\n\n  fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(v as i64)\n  }\n\n  fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(v as i64)\n  }\n\n  fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(v as i64)\n  }\n\n  fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(v)\n  }\n\n  fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(v as i64)\n  }\n\n  fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(v as i64)\n  }\n\n  fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(v as i64)\n  }\n\n  fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(v as i64)\n  }\n}\n\nstruct OptionMaybeStringI64Visitor;\n\nimpl Visitor<'_> for OptionMaybeStringI64Visitor {\n  type Value = Option<i64>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"null or number or string number\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    MaybeStringI64Visitor.visit_str(v).map(Some)\n  }\n\n  fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(Some(v as i64))\n  }\n\n  fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(Some(v as i64))\n  }\n\n  fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(Some(v as i64))\n  }\n\n  fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(Some(v as i64))\n  }\n\n  fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(Some(v as i64))\n  }\n\n  fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(Some(v))\n  }\n\n  fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(Some(v as i64))\n  }\n\n  fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(Some(v as i64))\n  }\n\n  fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(Some(v as i64))\n  }\n\n  fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(Some(v as i64))\n  }\n\n  fn visit_none<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n\n  fn visit_unit<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/deserializers/mod.rs",
    "content": "//! Deserializers for custom behavior and backward compatibility.\n\nmod conversion;\nmod environment;\nmod file_contents;\nmod forgiving_vec;\nmod item_or_vec;\nmod labels;\nmod maybe_string_i64;\nmod permission;\nmod string_list;\nmod term_signal_labels;\n\npub use conversion::*;\npub use environment::*;\npub use file_contents::*;\npub use forgiving_vec::*;\npub use item_or_vec::*;\npub use labels::*;\npub use maybe_string_i64::*;\npub use string_list::*;\npub use term_signal_labels::*;\n"
  },
  {
    "path": "client/core/rs/src/deserializers/permission.rs",
    "content": "//! This is a module to deserialize [PermissionLevelAndSpecifics].\n//!\n//! ## As just [PermissionLevel]\n//! permission = \"Write\"\n//!\n//! ## As expanded with [SpecificPermission]\n//! permission = { level = \"Write\", specific = [\"Terminal\"] }\n\nuse std::str::FromStr;\n\nuse indexmap::IndexSet;\nuse serde::{\n  Deserialize, Serialize,\n  de::{Visitor, value::MapAccessDeserializer},\n};\n\nuse crate::entities::permission::{\n  PermissionLevel, PermissionLevelAndSpecifics, SpecificPermission,\n};\n\n#[derive(Serialize, Deserialize)]\nstruct _PermissionLevelAndSpecifics {\n  #[serde(default)]\n  level: PermissionLevel,\n  #[serde(default)]\n  specific: IndexSet<SpecificPermission>,\n}\n\nimpl Serialize for PermissionLevelAndSpecifics {\n  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n  where\n    S: serde::Serializer,\n  {\n    if self.specific.is_empty() {\n      // Serialize to simple string\n      self.level.serialize(serializer)\n    } else {\n      _PermissionLevelAndSpecifics {\n        level: self.level,\n        specific: self.specific.clone(),\n      }\n      .serialize(serializer)\n    }\n  }\n}\n\nimpl<'de> Deserialize<'de> for PermissionLevelAndSpecifics {\n  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n  where\n    D: serde::Deserializer<'de>,\n  {\n    deserializer.deserialize_any(PermissionLevelAndSpecificsVisitor)\n  }\n}\n\nstruct PermissionLevelAndSpecificsVisitor;\n\nimpl<'de> Visitor<'de> for PermissionLevelAndSpecificsVisitor {\n  type Value = PermissionLevelAndSpecifics;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(\n      formatter,\n      \"PermissionLevel or PermissionLevelAndSpecifics\"\n    )\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(PermissionLevelAndSpecifics {\n      level: PermissionLevel::from_str(v)\n        .map_err(|e| serde::de::Error::custom(e))?,\n      specific: IndexSet::new(),\n    })\n  }\n\n  fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::MapAccess<'de>,\n  {\n    _PermissionLevelAndSpecifics::deserialize(\n      MapAccessDeserializer::new(map),\n    )\n    .map(|p| PermissionLevelAndSpecifics {\n      level: p.level,\n      specific: p.specific,\n    })\n  }\n\n  fn visit_unit<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(PermissionLevelAndSpecifics {\n      level: PermissionLevel::None,\n      specific: IndexSet::new(),\n    })\n  }\n\n  fn visit_none<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    self.visit_unit()\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/deserializers/string_list.rs",
    "content": "use serde::{\n  Deserialize, Deserializer,\n  de::{SeqAccess, Visitor, value::SeqAccessDeserializer},\n};\n\nuse crate::parsers::parse_string_list;\n\npub fn string_list_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<Vec<String>, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(StringListVisitor)\n}\n\npub fn option_string_list_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<Option<Vec<String>>, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(OptionStringListVisitor)\n}\n\nstruct StringListVisitor;\n\nimpl<'de> Visitor<'de> for StringListVisitor {\n  type Value = Vec<String>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"string or Vec<String>\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(parse_string_list(v))\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    Vec::<String>::deserialize(SeqAccessDeserializer::new(seq))\n  }\n}\n\nstruct OptionStringListVisitor;\n\nimpl<'de> Visitor<'de> for OptionStringListVisitor {\n  type Value = Option<Vec<String>>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"null or string or Vec<String>\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    StringListVisitor.visit_str(v).map(Some)\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: SeqAccess<'de>,\n  {\n    StringListVisitor.visit_seq(seq).map(Some)\n  }\n\n  fn visit_none<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n\n  fn visit_unit<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/deserializers/term_signal_labels.rs",
    "content": "use serde::{\n  Deserialize, Deserializer,\n  de::{Visitor, value::SeqAccessDeserializer},\n};\n\nuse crate::entities::deployment::TerminationSignalLabel;\n\npub fn term_labels_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<String, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(TermSignalLabelVisitor)\n}\n\npub fn option_term_labels_deserializer<'de, D>(\n  deserializer: D,\n) -> Result<Option<String>, D::Error>\nwhere\n  D: Deserializer<'de>,\n{\n  deserializer.deserialize_any(OptionTermSignalLabelVisitor)\n}\n\nstruct TermSignalLabelVisitor;\n\nimpl<'de> Visitor<'de> for TermSignalLabelVisitor {\n  type Value = String;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"string or Vec<TerminationSignalLabel>\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    let out = v.to_string();\n    if out.is_empty() || out.ends_with('\\n') {\n      Ok(out)\n    } else {\n      Ok(out + \"\\n\")\n    }\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    let res = Vec::<TerminationSignalLabel>::deserialize(\n      SeqAccessDeserializer::new(seq),\n    )?\n    .into_iter()\n    .map(|TerminationSignalLabel { signal, label }| {\n      format!(\"  {signal}: {label}\")\n    })\n    .collect::<Vec<_>>()\n    .join(\"\\n\");\n    let extra = if res.is_empty() { \"\" } else { \"\\n\" };\n    Ok(res + extra)\n  }\n}\n\nstruct OptionTermSignalLabelVisitor;\n\nimpl<'de> Visitor<'de> for OptionTermSignalLabelVisitor {\n  type Value = Option<String>;\n\n  fn expecting(\n    &self,\n    formatter: &mut std::fmt::Formatter,\n  ) -> std::fmt::Result {\n    write!(formatter, \"null or string or Vec<TerminationSignalLabel>\")\n  }\n\n  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    TermSignalLabelVisitor.visit_str(v).map(Some)\n  }\n\n  fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>\n  where\n    A: serde::de::SeqAccess<'de>,\n  {\n    TermSignalLabelVisitor.visit_seq(seq).map(Some)\n  }\n\n  fn visit_none<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n\n  fn visit_unit<E>(self) -> Result<Self::Value, E>\n  where\n    E: serde::de::Error,\n  {\n    Ok(None)\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/action.rs",
    "content": "use bson::{Document, doc};\nuse derive_builder::Builder;\nuse derive_default_builder::DefaultBuilder;\nuse partial_derive2::Partial;\nuse serde::{Deserialize, Serialize};\nuse strum::Display;\nuse typeshare::typeshare;\n\nuse crate::{\n  deserializers::{\n    file_contents_deserializer, option_file_contents_deserializer,\n  },\n  entities::{FileFormat, I64, NoData},\n};\n\nuse super::{\n  ScheduleFormat,\n  resource::{Resource, ResourceListItem, ResourceQuery},\n};\n\n#[typeshare]\npub type ActionListItem = ResourceListItem<ActionListItemInfo>;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct ActionListItemInfo {\n  /// Whether last action run successful\n  pub state: ActionState,\n  /// Action last successful run timestamp in ms.\n  pub last_run_at: Option<I64>,\n  /// If the action has schedule enabled, this is the\n  /// next scheduled run time in unix ms.\n  pub next_scheduled_run: Option<I64>,\n  /// If there is an error parsing schedule expression,\n  /// it will be given here.\n  pub schedule_error: Option<String>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  Default,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n  Display,\n  Serialize,\n  Deserialize,\n)]\npub enum ActionState {\n  /// Unknown case\n  #[default]\n  Unknown,\n  /// Last clone / pull successful (or never cloned)\n  Ok,\n  /// Last clone / pull failed\n  Failed,\n  /// Currently running\n  Running,\n}\n\n#[typeshare]\npub type Action = Resource<ActionConfig, NoData>;\n\n#[typeshare(serialized_as = \"Partial<ActionConfig>\")]\npub type _PartialActionConfig = PartialActionConfig;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[partial(skip_serializing_none, from, diff)]\npub struct ActionConfig {\n  /// Whether this action should run at startup.\n  #[serde(default = \"default_run_at_startup\")]\n  #[builder(default = \"default_run_at_startup()\")]\n  #[partial_default(default_run_at_startup())]\n  pub run_at_startup: bool,\n\n  /// Choose whether to specify schedule as regular CRON, or using the english to CRON parser.\n  #[serde(default)]\n  #[builder(default)]\n  pub schedule_format: ScheduleFormat,\n\n  /// Optionally provide a schedule for the procedure to run on.\n  ///\n  /// There are 2 ways to specify a schedule:\n  ///\n  /// 1. Regular CRON expression:\n  ///\n  /// (second, minute, hour, day, month, day-of-week)\n  /// ```text\n  /// 0 0 0 1,15 * ?\n  /// ```\n  ///\n  /// 2. \"English\" expression via [english-to-cron](https://crates.io/crates/english-to-cron):\n  ///\n  /// ```text\n  /// at midnight on the 1st and 15th of the month\n  /// ```\n  #[serde(default)]\n  #[builder(default)]\n  pub schedule: String,\n\n  /// Whether schedule is enabled if one is provided.\n  /// Can be used to temporarily disable the schedule.\n  #[serde(default = \"default_schedule_enabled\")]\n  #[builder(default = \"default_schedule_enabled()\")]\n  #[partial_default(default_schedule_enabled())]\n  pub schedule_enabled: bool,\n\n  /// Optional. A TZ Identifier. If not provided, will use Core local timezone.\n  /// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n  #[serde(default)]\n  #[builder(default)]\n  pub schedule_timezone: String,\n\n  /// Whether to send alerts when the schedule was run.\n  #[serde(default = \"default_schedule_alert\")]\n  #[builder(default = \"default_schedule_alert()\")]\n  #[partial_default(default_schedule_alert())]\n  pub schedule_alert: bool,\n\n  /// Whether to send alerts when this action fails.\n  #[serde(default = \"default_failure_alert\")]\n  #[builder(default = \"default_failure_alert()\")]\n  #[partial_default(default_failure_alert())]\n  pub failure_alert: bool,\n\n  /// Whether incoming webhooks actually trigger action.\n  #[serde(default = \"default_webhook_enabled\")]\n  #[builder(default = \"default_webhook_enabled()\")]\n  #[partial_default(default_webhook_enabled())]\n  pub webhook_enabled: bool,\n\n  /// Optionally provide an alternate webhook secret for this procedure.\n  /// If its an empty string, use the default secret from the config.\n  #[serde(default)]\n  #[builder(default)]\n  pub webhook_secret: String,\n\n  /// Whether deno will be instructed to reload all dependencies,\n  /// this can usually be kept false outside of development.\n  #[serde(default)]\n  #[builder(default)]\n  pub reload_deno_deps: bool,\n\n  /// Typescript file contents using pre-initialized `komodo` client.\n  /// Supports variable / secret interpolation.\n  #[serde(default, deserialize_with = \"file_contents_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_file_contents_deserializer\"\n  ))]\n  #[builder(default)]\n  pub file_contents: String,\n\n  /// Specify the format in which the arguments are defined.\n  /// Default: `key_value` (like environment)\n  #[serde(default)]\n  #[builder(default)]\n  pub arguments_format: FileFormat,\n\n  /// Default arguments to give to the Action for use in the script at `ARGS`.\n  #[serde(default, deserialize_with = \"file_contents_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_file_contents_deserializer\"\n  ))]\n  #[builder(default)]\n  pub arguments: String,\n}\n\nfn default_schedule_enabled() -> bool {\n  true\n}\n\nfn default_schedule_alert() -> bool {\n  true\n}\n\nfn default_failure_alert() -> bool {\n  true\n}\n\nfn default_run_at_startup() -> bool {\n  false\n}\n\nfn default_webhook_enabled() -> bool {\n  true\n}\n\nimpl ActionConfig {\n  pub fn builder() -> ActionConfigBuilder {\n    ActionConfigBuilder::default()\n  }\n}\n\nimpl Default for ActionConfig {\n  fn default() -> Self {\n    Self {\n      schedule_format: Default::default(),\n      schedule: Default::default(),\n      schedule_enabled: default_schedule_enabled(),\n      schedule_timezone: Default::default(),\n      run_at_startup: default_run_at_startup(),\n      schedule_alert: default_schedule_alert(),\n      failure_alert: default_failure_alert(),\n      webhook_enabled: default_webhook_enabled(),\n      webhook_secret: Default::default(),\n      reload_deno_deps: Default::default(),\n      arguments_format: Default::default(),\n      file_contents: Default::default(),\n      arguments: Default::default(),\n    }\n  }\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]\npub struct ActionActionState {\n  /// Number of instances of the Action currently running\n  pub running: u32,\n}\n\n#[typeshare]\npub type ActionQuery = ResourceQuery<ActionQuerySpecifics>;\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,\n)]\npub struct ActionQuerySpecifics {}\n\nimpl super::resource::AddFilters for ActionQuerySpecifics {\n  fn add_filters(&self, _filters: &mut Document) {}\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/alert.rs",
    "content": "use std::path::PathBuf;\n\nuse derive_variants::EnumVariants;\nuse serde::{Deserialize, Serialize};\nuse strum::{Display, EnumString};\nuse typeshare::typeshare;\n\nuse crate::entities::{I64, MongoId};\n\nuse super::{\n  _Serror, ResourceTarget, ResourceTargetVariant, Version,\n  deployment::DeploymentState, stack::StackState,\n};\n\n/// Representation of an alert in the system.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\n#[cfg_attr(feature = \"mongo\", doc_index({ \"data.type\": 1 }))]\n#[cfg_attr(feature = \"mongo\", doc_index({ \"target.type\": 1 }))]\n#[cfg_attr(feature = \"mongo\", doc_index({ \"target.id\": 1 }))]\npub struct Alert {\n  /// The Mongo ID of the alert.\n  /// This field is de/serialized from/to JSON as\n  /// `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Alert) }`\n  #[serde(\n    default,\n    rename = \"_id\",\n    skip_serializing_if = \"String::is_empty\",\n    with = \"bson::serde_helpers::hex_string_as_object_id\"\n  )]\n  pub id: MongoId,\n\n  /// Unix timestamp in milliseconds the alert was opened\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub ts: I64,\n\n  /// Whether the alert is already resolved\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub resolved: bool,\n\n  /// The severity of the alert\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub level: SeverityLevel,\n\n  /// The target of the alert\n  pub target: ResourceTarget,\n\n  /// The data attached to the alert\n  pub data: AlertData,\n\n  /// The timestamp of alert resolution\n  pub resolved_ts: Option<I64>,\n}\n\n/// The variants of data related to the alert.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, EnumVariants)]\n#[variant_derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  Hash\n)]\n#[serde(tag = \"type\", content = \"data\")]\npub enum AlertData {\n  /// A null alert\n  None {},\n\n  /// The user triggered a test of the\n  /// Alerter configuration.\n  Test {\n    /// The id of the alerter\n    id: String,\n    /// The name of the alerter\n    name: String,\n  },\n\n  /// A server could not be reached.\n  ServerUnreachable {\n    /// The id of the server\n    id: String,\n    /// The name of the server\n    name: String,\n    /// The region of the server\n    region: Option<String>,\n    /// The error data\n    err: Option<_Serror>,\n  },\n\n  /// A server has high CPU usage.\n  ServerCpu {\n    /// The id of the server\n    id: String,\n    /// The name of the server\n    name: String,\n    /// The region of the server\n    region: Option<String>,\n    /// The cpu usage percentage\n    percentage: f64,\n  },\n\n  /// A server has high memory usage.\n  ServerMem {\n    /// The id of the server\n    id: String,\n    /// The name of the server\n    name: String,\n    /// The region of the server\n    region: Option<String>,\n    /// The used memory\n    used_gb: f64,\n    /// The total memory\n    total_gb: f64,\n  },\n\n  /// A server has high disk usage.\n  ServerDisk {\n    /// The id of the server\n    id: String,\n    /// The name of the server\n    name: String,\n    /// The region of the server\n    region: Option<String>,\n    /// The mount path of the disk\n    path: PathBuf,\n    /// The used portion of the disk in GB\n    used_gb: f64,\n    /// The total size of the disk in GB\n    total_gb: f64,\n  },\n\n  /// A server has a version mismatch with the core.\n  ServerVersionMismatch {\n    /// The id of the server\n    id: String,\n    /// The name of the server\n    name: String,\n    /// The region of the server\n    region: Option<String>,\n    /// The actual server version\n    server_version: String,\n    /// The core version\n    core_version: String,\n  },\n\n  /// A container's state has changed unexpectedly.\n  ContainerStateChange {\n    /// The id of the deployment\n    id: String,\n    /// The name of the deployment\n    name: String,\n    /// The server id of server that the deployment is on\n    server_id: String,\n    /// The server name\n    server_name: String,\n    /// The previous container state\n    from: DeploymentState,\n    /// The current container state\n    to: DeploymentState,\n  },\n\n  /// A Deployment has an image update available\n  DeploymentImageUpdateAvailable {\n    /// The id of the deployment\n    id: String,\n    /// The name of the deployment\n    name: String,\n    /// The server id of server that the deployment is on\n    server_id: String,\n    /// The server name\n    server_name: String,\n    /// The image with update\n    image: String,\n  },\n\n  /// A Deployment has an image update available\n  DeploymentAutoUpdated {\n    /// The id of the deployment\n    id: String,\n    /// The name of the deployment\n    name: String,\n    /// The server id of server that the deployment is on\n    server_id: String,\n    /// The server name\n    server_name: String,\n    /// The updated image\n    image: String,\n  },\n\n  /// A stack's state has changed unexpectedly.\n  StackStateChange {\n    /// The id of the stack\n    id: String,\n    /// The name of the stack\n    name: String,\n    /// The server id of server that the stack is on\n    server_id: String,\n    /// The server name\n    server_name: String,\n    /// The previous stack state\n    from: StackState,\n    /// The current stack state\n    to: StackState,\n  },\n\n  /// A Stack has an image update available\n  StackImageUpdateAvailable {\n    /// The id of the stack\n    id: String,\n    /// The name of the stack\n    name: String,\n    /// The server id of server that the stack is on\n    server_id: String,\n    /// The server name\n    server_name: String,\n    /// The service name to update\n    service: String,\n    /// The image with update\n    image: String,\n  },\n\n  /// A Stack was auto updated\n  StackAutoUpdated {\n    /// The id of the stack\n    id: String,\n    /// The name of the stack\n    name: String,\n    /// The server id of server that the stack is on\n    server_id: String,\n    /// The server name\n    server_name: String,\n    /// One or more images that were updated\n    images: Vec<String>,\n  },\n\n  /// An AWS builder failed to terminate.\n  AwsBuilderTerminationFailed {\n    /// The id of the aws instance which failed to terminate\n    instance_id: String,\n    /// A reason for the failure\n    message: String,\n  },\n\n  /// A resource sync has pending updates\n  ResourceSyncPendingUpdates {\n    /// The id of the resource sync\n    id: String,\n    /// The name of the resource sync\n    name: String,\n  },\n\n  /// A build has failed\n  BuildFailed {\n    /// The id of the build\n    id: String,\n    /// The name of the build\n    name: String,\n    /// The version that failed to build\n    version: Version,\n  },\n\n  /// A repo has failed\n  RepoBuildFailed {\n    /// The id of the repo\n    id: String,\n    /// The name of the repo\n    name: String,\n  },\n\n  /// A procedure has failed\n  ProcedureFailed {\n    /// The id of the procedure\n    id: String,\n    /// The name of the procedure\n    name: String,\n  },\n\n  /// An action has failed\n  ActionFailed {\n    /// The id of the action\n    id: String,\n    /// The name of the action\n    name: String,\n  },\n\n  /// A schedule was run\n  ScheduleRun {\n    /// Procedure or Action\n    resource_type: ResourceTargetVariant,\n    /// The resource id\n    id: String,\n    /// The resource name\n    name: String,\n  },\n\n  /// Custom header / body.\n  /// Produced using `/execute/SendAlert`\n  Custom {\n    /// The alert message.\n    message: String,\n    /// Message details. May be empty string.\n    #[serde(default)]\n    details: String,\n  },\n}\n\nimpl Default for AlertData {\n  fn default() -> Self {\n    AlertData::None {}\n  }\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for AlertDataVariant {\n  fn default() -> Self {\n    AlertDataVariant::None\n  }\n}\n\n/// Severity level of problem.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Default,\n  Display,\n  EnumString,\n)]\n#[serde(rename_all = \"UPPERCASE\")]\n#[strum(serialize_all = \"UPPERCASE\")]\npub enum SeverityLevel {\n  /// No problem.\n  ///\n  /// Aliases: ok, low, l\n  #[default]\n  #[strum(serialize = \"ok\", serialize = \"low\", serialize = \"l\")]\n  Ok,\n  /// Problem is imminent.\n  ///\n  /// Aliases: warning, w, medium, m\n  #[strum(\n    serialize = \"warning\",\n    serialize = \"w\",\n    serialize = \"medium\",\n    serialize = \"m\"\n  )]\n  Warning,\n  /// Problem fully realized.\n  ///\n  /// Aliases: critical, c, high, h\n  #[strum(\n    serialize = \"critical\",\n    serialize = \"c\",\n    serialize = \"high\",\n    serialize = \"h\"\n  )]\n  Critical,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/alerter.rs",
    "content": "use bson::{Document, doc};\nuse derive_builder::Builder;\nuse derive_default_builder::DefaultBuilder;\nuse derive_variants::EnumVariants;\nuse partial_derive2::Partial;\nuse serde::{Deserialize, Serialize};\nuse strum::{AsRefStr, Display, EnumString};\nuse typeshare::typeshare;\n\nuse crate::entities::MaintenanceWindow;\n\nuse super::{\n  ResourceTarget,\n  alert::AlertDataVariant,\n  resource::{Resource, ResourceListItem, ResourceQuery},\n};\n\n#[typeshare]\npub type Alerter = Resource<AlerterConfig, ()>;\n\n#[typeshare]\npub type AlerterListItem = ResourceListItem<AlerterListItemInfo>;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct AlerterListItemInfo {\n  /// Whether alerter is enabled for sending alerts\n  pub enabled: bool,\n  /// The type of the alerter, eg. `Slack`, `Custom`\n  pub endpoint_type: AlerterEndpointVariant,\n}\n\n#[typeshare(serialized_as = \"Partial<AlerterConfig>\")]\npub type _PartialAlerterConfig = PartialAlerterConfig;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[partial(skip_serializing_none, from, diff)]\npub struct AlerterConfig {\n  /// Whether the alerter is enabled\n  #[serde(default)]\n  #[builder(default)]\n  pub enabled: bool,\n\n  /// Where to route the alert messages.\n  ///\n  /// Default: Custom endpoint `http://localhost:7000`\n  #[serde(default)]\n  #[builder(default)]\n  pub endpoint: AlerterEndpoint,\n\n  /// Only send specific alert types.\n  /// If empty, will send all alert types.\n  #[serde(default)]\n  #[builder(default)]\n  pub alert_types: Vec<AlertDataVariant>,\n\n  /// Only send alerts on specific resources.\n  /// If empty, will send alerts for all resources.\n  #[serde(default)]\n  #[builder(default)]\n  pub resources: Vec<ResourceTarget>,\n\n  /// DON'T send alerts on these resources.\n  #[serde(default)]\n  #[builder(default)]\n  pub except_resources: Vec<ResourceTarget>,\n\n  /// Scheduled maintenance windows during which alerts will be suppressed.\n  #[serde(default)]\n  #[builder(default)]\n  pub maintenance_windows: Vec<MaintenanceWindow>,\n}\n\nimpl AlerterConfig {\n  pub fn builder() -> AlerterConfigBuilder {\n    AlerterConfigBuilder::default()\n  }\n}\n\n#[allow(clippy::derivable_impls)]\nimpl Default for AlerterConfig {\n  fn default() -> Self {\n    Self {\n      enabled: Default::default(),\n      endpoint: Default::default(),\n      alert_types: Default::default(),\n      resources: Default::default(),\n      except_resources: Default::default(),\n      maintenance_windows: Default::default(),\n    }\n  }\n}\n\n// ENDPOINTS\n\n#[typeshare]\n#[derive(\n  Debug, Clone, PartialEq, Serialize, Deserialize, EnumVariants,\n)]\n#[variant_derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n  Display,\n  EnumString,\n  AsRefStr,\n  Serialize,\n  Deserialize\n)]\n#[serde(tag = \"type\", content = \"params\")]\npub enum AlerterEndpoint {\n  /// Send alert serialized to JSON to an http endpoint.\n  Custom(CustomAlerterEndpoint),\n\n  /// Send alert to a Slack app\n  Slack(SlackAlerterEndpoint),\n\n  /// Send alert to a Discord app\n  Discord(DiscordAlerterEndpoint),\n\n  /// Send alert to Ntfy\n  Ntfy(NtfyAlerterEndpoint),\n\n  /// Send alert to Pushover\n  Pushover(PushoverAlerterEndpoint),\n}\n\nimpl Default for AlerterEndpoint {\n  fn default() -> Self {\n    Self::Custom(Default::default())\n  }\n}\n\n/// Configuration for a Custom alerter endpoint.\n#[typeshare]\n#[derive(\n  Debug, Clone, PartialEq, Serialize, Deserialize, Builder,\n)]\npub struct CustomAlerterEndpoint {\n  /// The http/s endpoint to send the POST to\n  #[serde(default = \"default_custom_url\")]\n  #[builder(default = \"default_custom_url()\")]\n  pub url: String,\n}\n\nimpl Default for CustomAlerterEndpoint {\n  fn default() -> Self {\n    Self {\n      url: default_custom_url(),\n    }\n  }\n}\n\nfn default_custom_url() -> String {\n  String::from(\"http://localhost:7000\")\n}\n\n/// Configuration for a Slack alerter.\n#[typeshare]\n#[derive(\n  Debug, Clone, PartialEq, Serialize, Deserialize, Builder,\n)]\npub struct SlackAlerterEndpoint {\n  /// The Slack app webhook url\n  #[serde(default = \"default_slack_url\")]\n  #[builder(default = \"default_slack_url()\")]\n  pub url: String,\n}\n\nimpl Default for SlackAlerterEndpoint {\n  fn default() -> Self {\n    Self {\n      url: default_slack_url(),\n    }\n  }\n}\n\nfn default_slack_url() -> String {\n  String::from(\n    \"https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX\",\n  )\n}\n\n/// Configuration for a Discord alerter.\n#[typeshare]\n#[derive(\n  Debug, Clone, PartialEq, Serialize, Deserialize, Builder,\n)]\npub struct DiscordAlerterEndpoint {\n  /// The Discord webhook url\n  #[serde(default = \"default_discord_url\")]\n  #[builder(default = \"default_discord_url()\")]\n  pub url: String,\n}\n\nimpl Default for DiscordAlerterEndpoint {\n  fn default() -> Self {\n    Self {\n      url: default_discord_url(),\n    }\n  }\n}\n\nfn default_discord_url() -> String {\n  String::from(\n    \"https://discord.com/api/webhooks/XXXXXXXXXXXX/XXXX-XXXXXXXXXX\",\n  )\n}\n\n/// Configuration for a Ntfy alerter.\n#[typeshare]\n#[derive(\n  Debug, Clone, PartialEq, Serialize, Deserialize, Builder,\n)]\npub struct NtfyAlerterEndpoint {\n  /// The ntfy topic URL\n  #[serde(default = \"default_ntfy_url\")]\n  #[builder(default = \"default_ntfy_url()\")]\n  pub url: String,\n\n  /// Optional E-Mail Address to enable ntfy email notifications.\n  /// SMTP must be configured on the ntfy server.\n  pub email: Option<String>,\n}\n\nimpl Default for NtfyAlerterEndpoint {\n  fn default() -> Self {\n    Self {\n      url: default_ntfy_url(),\n      email: None,\n    }\n  }\n}\n\nfn default_ntfy_url() -> String {\n  String::from(\"http://localhost:8080/komodo\")\n}\n\n/// Configuration for a Pushover alerter.\n#[typeshare]\n#[derive(\n  Debug, Clone, PartialEq, Serialize, Deserialize, Builder,\n)]\npub struct PushoverAlerterEndpoint {\n  /// The pushover URL including application and user tokens in parameters.\n  #[serde(default = \"default_pushover_url\")]\n  #[builder(default = \"default_pushover_url()\")]\n  pub url: String,\n}\n\nimpl Default for PushoverAlerterEndpoint {\n  fn default() -> Self {\n    Self {\n      url: default_pushover_url(),\n    }\n  }\n}\n\nfn default_pushover_url() -> String {\n  String::from(\n    \"https://api.pushover.net/1/messages.json?token=XXXXXXXXXXXXX&user=XXXXXXXXXXXXX\",\n  )\n}\n\n// QUERY\n#[typeshare]\npub type AlerterQuery = ResourceQuery<AlerterQuerySpecifics>;\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,\n)]\npub struct AlerterQuerySpecifics {\n  /// Filter alerters by enabled.\n  /// - `None`: Don't filter by enabled\n  /// - `Some(true)`: Only include alerts with `enabled: true`\n  /// - `Some(false)`: Only include alerts with `enabled: false`\n  pub enabled: Option<bool>,\n\n  /// Only include alerters with these endpoint types.\n  /// If empty, don't filter by enpoint type.\n  pub types: Vec<AlerterEndpointVariant>,\n}\n\nimpl super::resource::AddFilters for AlerterQuerySpecifics {\n  fn add_filters(&self, filters: &mut Document) {\n    if let Some(enabled) = self.enabled {\n      filters.insert(\"config.enabled\", enabled);\n    }\n    let types =\n      self.types.iter().map(|t| t.as_ref()).collect::<Vec<_>>();\n    if !self.types.is_empty() {\n      filters.insert(\"config.endpoint.type\", doc! { \"$in\": types });\n    }\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/api_key.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse super::I64;\n\n/// An api key used to authenticate requests via request headers.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\npub struct ApiKey {\n  /// Unique key associated with secret\n  #[cfg_attr(feature = \"mongo\", unique_index)]\n  pub key: String,\n\n  /// Hash of the secret\n  pub secret: String,\n\n  /// User associated with the api key\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub user_id: String,\n\n  /// Name associated with the api key for management\n  pub name: String,\n\n  /// Timestamp of key creation\n  pub created_at: I64,\n\n  /// Expiry of key, or 0 if never expires\n  pub expires: I64,\n}\n\nimpl ApiKey {\n  pub fn sanitize(&mut self) {\n    self.secret.clear()\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/build.rs",
    "content": "use std::{fmt::Write, sync::OnceLock};\n\nuse bson::{Document, doc};\nuse derive_builder::Builder;\nuse derive_default_builder::DefaultBuilder;\nuse partial_derive2::Partial;\nuse serde::{Deserialize, Serialize};\nuse strum::Display;\nuse typeshare::typeshare;\n\nuse crate::{\n  deserializers::{\n    env_vars_deserializer, item_or_vec_deserializer,\n    labels_deserializer, option_env_vars_deserializer,\n    option_item_or_vec_deserializer, option_labels_deserializer,\n    option_string_list_deserializer, string_list_deserializer,\n  },\n  entities::I64,\n};\n\nuse super::{\n  SystemCommand, Version,\n  resource::{Resource, ResourceListItem, ResourceQuery},\n};\n\n#[typeshare]\npub type Build = Resource<BuildConfig, BuildInfo>;\n\nimpl Build {\n  pub fn get_image_names(&self) -> Vec<String> {\n    let Build {\n      name,\n      config:\n        BuildConfig {\n          image_name,\n          image_registry,\n          ..\n        },\n      ..\n    } = self;\n    let name = if image_name.is_empty() {\n      name\n    } else {\n      image_name\n    };\n    // Local only\n    if image_registry.is_empty() {\n      return vec![name.to_string()];\n    }\n    image_registry\n      .iter()\n      .map(\n        |ImageRegistryConfig {\n           domain,\n           account,\n           organization,\n         }| {\n          match (\n            !domain.is_empty(),\n            !organization.is_empty(),\n            !account.is_empty(),\n          ) {\n            // If organization and account provided, name under organization.\n            (true, true, true) => {\n              format!(\"{domain}/{organization}/{name}\")\n            }\n            // Just domain / account provided\n            (true, false, true) => {\n              format!(\"{domain}/{account}/{name}\")\n            }\n            // Otherwise, just use name (local only)\n            _ => name.to_string(),\n          }\n        },\n      )\n      .collect()\n  }\n\n  pub fn get_image_tags(\n    &self,\n    image_names: &[String],\n    commit_hash: Option<&str>,\n    additional: &[String],\n  ) -> Vec<String> {\n    let BuildConfig {\n      version,\n      image_tag,\n      include_latest_tag,\n      include_version_tags: include_version_tag,\n      include_commit_tag,\n      ..\n    } = &self.config;\n\n    let Version { major, minor, .. } = version;\n\n    let image_tag_postfix = if image_tag.is_empty() {\n      String::new()\n    } else {\n      format!(\"-{image_tag}\")\n    };\n\n    let mut tags = Vec::new();\n\n    for image_name in image_names {\n      // Pure image tag passthrough when provided\n      if !image_tag.is_empty() {\n        tags.push(format!(\"{image_name}:{image_tag}\"));\n      }\n      // `:latest` / `:latest-tag`\n      if *include_latest_tag {\n        tags.push(format!(\"{image_name}:latest{image_tag_postfix}\"));\n      }\n      // `:1.19.5` + `:1.19` etc. / `1.19.5-tag`\n      if *include_version_tag {\n        tags\n          .push(format!(\"{image_name}:{version}{image_tag_postfix}\"));\n        tags.push(format!(\n          \"{image_name}:{major}.{minor}{image_tag_postfix}\"\n        ));\n        tags.push(format!(\"{image_name}:{major}{image_tag_postfix}\"));\n      }\n      if *include_commit_tag && let Some(hash) = commit_hash {\n        tags.push(format!(\"{image_name}:{hash}{image_tag_postfix}\"));\n      }\n      for tag in additional {\n        tags.push(format!(\"{image_name}:{tag}\"))\n      }\n    }\n\n    tags\n  }\n\n  pub fn get_image_tags_as_arg(\n    &self,\n    commit_hash: Option<&str>,\n    additional: &[String],\n  ) -> anyhow::Result<String> {\n    let mut res = String::new();\n    for image_tag in self.get_image_tags(\n      &self.get_image_names(),\n      commit_hash,\n      additional,\n    ) {\n      write!(&mut res, \" -t {image_tag}\")?;\n    }\n    Ok(res)\n  }\n}\n\n#[typeshare]\npub type BuildListItem = ResourceListItem<BuildListItemInfo>;\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct BuildListItemInfo {\n  /// State of the build. Reflects whether most recent build successful.\n  pub state: BuildState,\n  /// Unix timestamp in milliseconds of last build\n  pub last_built_at: I64,\n  /// The current version of the build\n  pub version: Version,\n  /// The builder attached to build.\n  pub builder_id: String,\n\n  /// Whether build is in files on host mode.\n  pub files_on_host: bool,\n  /// Whether build has UI defined dockerfile contents\n  pub dockerfile_contents: bool,\n\n  /// Linked repo, if one is attached.\n  pub linked_repo: String,\n  /// The git provider domain\n  pub git_provider: String,\n  /// The repo used as the source of the build\n  pub repo: String,\n  /// The branch of the repo\n  pub branch: String,\n  /// Full link to the repo.\n  pub repo_link: String,\n\n  /// Latest built short commit hash, or null.\n  pub built_hash: Option<String>,\n  /// Latest short commit hash, or null. Only for repo based stacks\n  pub latest_hash: Option<String>,\n\n  /// The first listed image registry domain\n  pub image_registry_domain: Option<String>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  Default,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n  Serialize,\n  Deserialize,\n  Display,\n)]\npub enum BuildState {\n  /// Currently building\n  Building,\n  /// Last build successful (or never built)\n  Ok,\n  /// Last build failed\n  Failed,\n  /// Other case\n  #[default]\n  Unknown,\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct BuildInfo {\n  /// The timestamp build was last built.\n  pub last_built_at: I64,\n\n  /// Latest built short commit hash, or null.\n  pub built_hash: Option<String>,\n  /// Latest built commit message, or null. Only for repo based stacks\n  pub built_message: Option<String>,\n  /// The last built dockerfile contents.\n  /// This is updated whenever Komodo successfully runs the build.\n  pub built_contents: Option<String>,\n\n  /// The absolute path to the file\n  pub remote_path: Option<String>,\n  /// The remote dockerfile contents, whether on host or in repo.\n  /// This is updated whenever Komodo refreshes the build cache.\n  /// It will be empty if the dockerfile is defined directly in the build config.\n  pub remote_contents: Option<String>,\n  /// If there was an error in getting the remote contents, it will be here.\n  pub remote_error: Option<String>,\n\n  /// Latest remote short commit hash, or null.\n  pub latest_hash: Option<String>,\n  /// Latest remote commit message, or null\n  pub latest_message: Option<String>,\n}\n\n#[typeshare(serialized_as = \"Partial<BuildConfig>\")]\npub type _PartialBuildConfig = PartialBuildConfig;\n\n/// The build configuration.\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize, Builder, Partial)]\n#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)]\n#[partial(skip_serializing_none, from, diff)]\npub struct BuildConfig {\n  /// Which builder is used to build the image.\n  #[serde(default, alias = \"builder\")]\n  #[partial_attr(serde(alias = \"builder\"))]\n  #[builder(default)]\n  pub builder_id: String,\n\n  /// The current version of the build.\n  #[serde(default)]\n  #[builder(default)]\n  pub version: Version,\n\n  /// Whether to automatically increment the patch on every build.\n  /// Default is `true`\n  #[serde(default = \"default_auto_increment_version\")]\n  #[builder(default = \"default_auto_increment_version()\")]\n  #[partial_default(default_auto_increment_version())]\n  pub auto_increment_version: bool,\n\n  /// An alternate name for the image pushed to the repository.\n  /// If this is empty, it will use the build name.\n  ///\n  /// Can be used in conjunction with `image_tag` to direct multiple builds\n  /// with different configs to push to the same image registry, under different,\n  /// independantly versioned tags.\n  #[serde(default)]\n  #[builder(default)]\n  pub image_name: String,\n\n  /// An extra tag put after the build version, for the image pushed to the repository.\n  /// Eg. in image tag of `aarch64` would push to moghtech/komodo-core:1.13.2-aarch64.\n  /// If this is empty, the image tag will just be the build version.\n  ///\n  /// Can be used in conjunction with `image_name` to direct multiple builds\n  /// with different configs to push to the same image registry, under different,\n  /// independantly versioned tags.\n  #[serde(default)]\n  #[builder(default)]\n  pub image_tag: String,\n\n  /// Push `:latest` / `:latest-image_tag` tags.\n  #[serde(default = \"default_include_tag\")]\n  #[builder(default = \"default_include_tag()\")]\n  #[partial_default(default_include_tag())]\n  pub include_latest_tag: bool,\n\n  /// Push build version semver `:1.19.5` + `1.19` / `:1.19.5-image_tag` tags.\n  #[serde(default = \"default_include_tag\")]\n  #[builder(default = \"default_include_tag()\")]\n  #[partial_default(default_include_tag())]\n  pub include_version_tags: bool,\n\n  /// Push commit hash `:a6v8h83` / `:a6v8h83-image_tag` tags.\n  #[serde(default = \"default_include_tag\")]\n  #[builder(default = \"default_include_tag()\")]\n  #[partial_default(default_include_tag())]\n  pub include_commit_tag: bool,\n\n  /// Configure quick links that are displayed in the resource header\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub links: Vec<String>,\n\n  /// Choose a Komodo Repo (Resource) to source the build files.\n  #[serde(default)]\n  #[builder(default)]\n  pub linked_repo: String,\n\n  /// The git provider domain. Default: github.com\n  #[serde(default = \"default_git_provider\")]\n  #[builder(default = \"default_git_provider()\")]\n  #[partial_default(default_git_provider())]\n  pub git_provider: String,\n\n  /// Whether to use https to clone the repo (versus http). Default: true\n  ///\n  /// Note. Komodo does not currently support cloning repos via ssh.\n  #[serde(default = \"default_git_https\")]\n  #[builder(default = \"default_git_https()\")]\n  #[partial_default(default_git_https())]\n  pub git_https: bool,\n\n  /// The git account used to access private repos.\n  /// Passing empty string can only clone public repos.\n  ///\n  /// Note. A token for the account must be available in the core config or the builder server's periphery config\n  /// for the configured git provider.\n  #[serde(default)]\n  #[builder(default)]\n  pub git_account: String,\n\n  /// The repo used as the source of the build.\n  #[serde(default)]\n  #[builder(default)]\n  pub repo: String,\n\n  /// The branch of the repo.\n  #[serde(default = \"default_branch\")]\n  #[builder(default = \"default_branch()\")]\n  #[partial_default(default_branch())]\n  pub branch: String,\n\n  /// Optionally set a specific commit hash.\n  #[serde(default)]\n  #[builder(default)]\n  pub commit: String,\n\n  /// Whether incoming webhooks actually trigger action.\n  #[serde(default = \"default_webhook_enabled\")]\n  #[builder(default = \"default_webhook_enabled()\")]\n  #[partial_default(default_webhook_enabled())]\n  pub webhook_enabled: bool,\n\n  /// Optionally provide an alternate webhook secret for this build.\n  /// If its an empty string, use the default secret from the config.\n  #[serde(default)]\n  #[builder(default)]\n  pub webhook_secret: String,\n\n  /// If this is checked, the build will source the files on the host.\n  /// Use `build_path` and `dockerfile_path` to specify the path on the host.\n  /// This is useful for those who wish to setup their files on the host,\n  /// rather than defining the contents in UI or in a git repo.\n  #[serde(default)]\n  #[builder(default)]\n  pub files_on_host: bool,\n\n  /// The path of the docker build context relative to the root of the repo.\n  /// Default: \".\" (the root of the repo).\n  #[serde(default = \"default_build_path\")]\n  #[builder(default = \"default_build_path()\")]\n  #[partial_default(default_build_path())]\n  pub build_path: String,\n\n  /// The path of the dockerfile relative to the build path.\n  #[serde(default = \"default_dockerfile_path\")]\n  #[builder(default = \"default_dockerfile_path()\")]\n  #[partial_default(default_dockerfile_path())]\n  pub dockerfile_path: String,\n\n  /// Configuration for the registry/s to push the built image to.\n  /// The first registry in this list will be used with attached Deployments.\n  #[serde(default, deserialize_with = \"item_or_vec_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_item_or_vec_deserializer\"\n  ))]\n  #[builder(default)]\n  pub image_registry: Vec<ImageRegistryConfig>,\n\n  /// Whether to skip secret interpolation in the build_args.\n  #[serde(default)]\n  #[builder(default)]\n  pub skip_secret_interp: bool,\n\n  /// Whether to use buildx to build (eg `docker buildx build ...`)\n  #[serde(default)]\n  #[builder(default)]\n  pub use_buildx: bool,\n\n  /// Any extra docker cli arguments to be included in the build command\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub extra_args: Vec<String>,\n\n  /// The optional command run after repo clone and before docker build.\n  #[serde(default)]\n  #[builder(default)]\n  pub pre_build: SystemCommand,\n\n  /// UI defined dockerfile contents.\n  /// Supports variable / secret interpolation.\n  #[serde(default)]\n  #[builder(default)]\n  pub dockerfile: String,\n\n  /// Docker build arguments.\n  ///\n  /// These values are visible in the final image by running `docker inspect`.\n  #[serde(default, deserialize_with = \"env_vars_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_env_vars_deserializer\"\n  ))]\n  #[builder(default)]\n  pub build_args: String,\n\n  /// Secret arguments.\n  ///\n  /// These values remain hidden in the final image by using\n  /// docker secret mounts. See <https://docs.docker.com/build/building/secrets>.\n  ///\n  /// The values can be used in RUN commands:\n  /// ```sh\n  /// RUN --mount=type=secret,id=SECRET_KEY \\\n  ///   SECRET_KEY=$(cat /run/secrets/SECRET_KEY) ...\n  /// ```\n  #[serde(default, deserialize_with = \"env_vars_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_env_vars_deserializer\"\n  ))]\n  #[builder(default)]\n  pub secret_args: String,\n\n  /// Docker labels\n  #[serde(default, deserialize_with = \"labels_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_labels_deserializer\"\n  ))]\n  #[builder(default)]\n  pub labels: String,\n}\n\nimpl BuildConfig {\n  pub fn builder() -> BuildConfigBuilder {\n    BuildConfigBuilder::default()\n  }\n}\n\nfn default_auto_increment_version() -> bool {\n  true\n}\n\nfn default_include_tag() -> bool {\n  true\n}\n\nfn default_git_provider() -> String {\n  String::from(\"github.com\")\n}\n\nfn default_git_https() -> bool {\n  true\n}\n\nfn default_branch() -> String {\n  String::from(\"main\")\n}\n\nfn default_build_path() -> String {\n  String::from(\".\")\n}\n\nfn default_dockerfile_path() -> String {\n  String::from(\"Dockerfile\")\n}\n\nfn default_webhook_enabled() -> bool {\n  true\n}\n\nimpl Default for BuildConfig {\n  fn default() -> Self {\n    Self {\n      builder_id: Default::default(),\n      skip_secret_interp: Default::default(),\n      version: Default::default(),\n      auto_increment_version: default_auto_increment_version(),\n      image_name: Default::default(),\n      image_tag: Default::default(),\n      include_latest_tag: default_include_tag(),\n      include_version_tags: default_include_tag(),\n      include_commit_tag: default_include_tag(),\n      links: Default::default(),\n      linked_repo: Default::default(),\n      git_provider: default_git_provider(),\n      git_https: default_git_https(),\n      repo: Default::default(),\n      branch: default_branch(),\n      commit: Default::default(),\n      git_account: Default::default(),\n      pre_build: Default::default(),\n      build_path: default_build_path(),\n      dockerfile_path: default_dockerfile_path(),\n      build_args: Default::default(),\n      secret_args: Default::default(),\n      labels: Default::default(),\n      extra_args: Default::default(),\n      use_buildx: Default::default(),\n      image_registry: Default::default(),\n      webhook_enabled: default_webhook_enabled(),\n      webhook_secret: Default::default(),\n      dockerfile: Default::default(),\n      files_on_host: Default::default(),\n    }\n  }\n}\n\n/// Configuration for an image registry\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ImageRegistryConfig {\n  /// Specify the registry provider domain, eg `docker.io`.\n  /// If not provided, will not push to any registry.\n  #[serde(default)]\n  pub domain: String,\n\n  /// Specify an account to use with the registry.\n  #[serde(default)]\n  pub account: String,\n\n  /// Optional. Specify an organization to push the image under.\n  /// Empty string means no organization.\n  #[serde(default)]\n  pub organization: String,\n}\n\nimpl ImageRegistryConfig {\n  pub fn static_default() -> &'static ImageRegistryConfig {\n    static DEFAULT: OnceLock<ImageRegistryConfig> = OnceLock::new();\n    DEFAULT.get_or_init(Default::default)\n  }\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]\npub struct BuildActionState {\n  pub building: bool,\n}\n\n#[typeshare]\npub type BuildQuery = ResourceQuery<BuildQuerySpecifics>;\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder,\n)]\npub struct BuildQuerySpecifics {\n  #[serde(default)]\n  pub builder_ids: Vec<String>,\n\n  #[serde(default)]\n  pub repos: Vec<String>,\n\n  /// query for builds last built more recently than this timestamp\n  /// defaults to 0 which is a no op\n  #[serde(default)]\n  pub built_since: I64,\n}\n\nimpl super::resource::AddFilters for BuildQuerySpecifics {\n  fn add_filters(&self, filters: &mut Document) {\n    if !self.builder_ids.is_empty() {\n      filters.insert(\n        \"config.builder_id\",\n        doc! { \"$in\": &self.builder_ids },\n      );\n    }\n    if !self.repos.is_empty() {\n      filters.insert(\"config.repo\", doc! { \"$in\": &self.repos });\n    }\n    if self.built_since > 0 {\n      filters.insert(\n        \"info.last_built_at\",\n        doc! { \"$gte\": self.built_since },\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/builder.rs",
    "content": "use derive_builder::Builder;\nuse derive_variants::EnumVariants;\nuse partial_derive2::{Diff, MaybeNone, Partial, PartialDiff};\nuse serde::{Deserialize, Serialize};\nuse strum::{Display, EnumString};\nuse typeshare::typeshare;\n\nuse crate::deserializers::{\n  option_string_list_deserializer, string_list_deserializer,\n};\n\nuse super::{\n  MergePartial,\n  config::{DockerRegistry, GitProvider},\n  resource::{AddFilters, Resource, ResourceListItem, ResourceQuery},\n};\n\n#[typeshare]\npub type Builder = Resource<BuilderConfig, ()>;\n\n#[typeshare]\npub type BuilderListItem = ResourceListItem<BuilderListItemInfo>;\n\n#[typeshare(serialized_as = \"Partial<BuilderConfig>\")]\npub type _PartialBuilderConfig = PartialBuilderConfig;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct BuilderListItemInfo {\n  /// 'Url', 'Server', or 'Aws'\n  pub builder_type: String,\n  /// If 'Url': null\n  /// If 'Server': the server id\n  /// If 'Aws': the instance type (eg. c5.xlarge)\n  pub instance_type: Option<String>,\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, EnumVariants)]\n#[variant_derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  Copy,\n  Display,\n  EnumString\n)]\n#[serde(tag = \"type\", content = \"params\")]\n#[allow(clippy::large_enum_variant)]\npub enum BuilderConfig {\n  /// Use a Periphery address as a Builder.\n  Url(UrlBuilderConfig),\n\n  /// Use a connected server as a Builder.\n  Server(ServerBuilderConfig),\n\n  /// Use EC2 instances spawned on demand as a Builder.\n  Aws(AwsBuilderConfig),\n}\n\nimpl Default for BuilderConfig {\n  fn default() -> Self {\n    Self::Aws(Default::default())\n  }\n}\n\n/// Partial representation of [BuilderConfig]\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, EnumVariants)]\n#[variant_derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  Copy,\n  Display,\n  EnumString\n)]\n#[serde(tag = \"type\", content = \"params\")]\n#[allow(clippy::large_enum_variant)]\npub enum PartialBuilderConfig {\n  Url(#[serde(default)] _PartialUrlBuilderConfig),\n  Server(#[serde(default)] _PartialServerBuilderConfig),\n  Aws(#[serde(default)] _PartialAwsBuilderConfig),\n}\n\nimpl Default for PartialBuilderConfig {\n  fn default() -> Self {\n    Self::Url(Default::default())\n  }\n}\n\nimpl MaybeNone for PartialBuilderConfig {\n  fn is_none(&self) -> bool {\n    match self {\n      PartialBuilderConfig::Url(config) => config.is_none(),\n      PartialBuilderConfig::Server(config) => config.is_none(),\n      PartialBuilderConfig::Aws(config) => config.is_none(),\n    }\n  }\n}\n\n#[allow(clippy::large_enum_variant)]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum BuilderConfigDiff {\n  Url(UrlBuilderConfigDiff),\n  Server(ServerBuilderConfigDiff),\n  Aws(AwsBuilderConfigDiff),\n}\n\nimpl From<BuilderConfigDiff> for PartialBuilderConfig {\n  fn from(value: BuilderConfigDiff) -> Self {\n    match value {\n      BuilderConfigDiff::Url(diff) => {\n        PartialBuilderConfig::Url(diff.into())\n      }\n      BuilderConfigDiff::Server(diff) => {\n        PartialBuilderConfig::Server(diff.into())\n      }\n      BuilderConfigDiff::Aws(diff) => {\n        PartialBuilderConfig::Aws(diff.into())\n      }\n    }\n  }\n}\n\nimpl Diff for BuilderConfigDiff {\n  fn iter_field_diffs(\n    &self,\n  ) -> impl Iterator<Item = partial_derive2::FieldDiff> {\n    match self {\n      BuilderConfigDiff::Url(diff) => {\n        diff.iter_field_diffs().collect::<Vec<_>>().into_iter()\n      }\n      BuilderConfigDiff::Server(diff) => {\n        diff.iter_field_diffs().collect::<Vec<_>>().into_iter()\n      }\n      BuilderConfigDiff::Aws(diff) => {\n        diff.iter_field_diffs().collect::<Vec<_>>().into_iter()\n      }\n    }\n  }\n}\n\nimpl PartialDiff<PartialBuilderConfig, BuilderConfigDiff>\n  for BuilderConfig\n{\n  fn partial_diff(\n    &self,\n    partial: PartialBuilderConfig,\n  ) -> BuilderConfigDiff {\n    match self {\n      BuilderConfig::Url(original) => match partial {\n        PartialBuilderConfig::Url(partial) => {\n          BuilderConfigDiff::Url(original.partial_diff(partial))\n        }\n        PartialBuilderConfig::Server(partial) => {\n          let default = ServerBuilderConfig::default();\n          BuilderConfigDiff::Server(default.partial_diff(partial))\n        }\n        PartialBuilderConfig::Aws(partial) => {\n          let default = AwsBuilderConfig::default();\n          BuilderConfigDiff::Aws(default.partial_diff(partial))\n        }\n      },\n      BuilderConfig::Server(original) => match partial {\n        PartialBuilderConfig::Server(partial) => {\n          BuilderConfigDiff::Server(original.partial_diff(partial))\n        }\n        PartialBuilderConfig::Url(partial) => {\n          let default = UrlBuilderConfig::default();\n          BuilderConfigDiff::Url(default.partial_diff(partial))\n        }\n        PartialBuilderConfig::Aws(partial) => {\n          let default = AwsBuilderConfig::default();\n          BuilderConfigDiff::Aws(default.partial_diff(partial))\n        }\n      },\n      BuilderConfig::Aws(original) => match partial {\n        PartialBuilderConfig::Aws(partial) => {\n          BuilderConfigDiff::Aws(original.partial_diff(partial))\n        }\n        PartialBuilderConfig::Url(partial) => {\n          let default = UrlBuilderConfig::default();\n          BuilderConfigDiff::Url(default.partial_diff(partial))\n        }\n        PartialBuilderConfig::Server(partial) => {\n          let default = ServerBuilderConfig::default();\n          BuilderConfigDiff::Server(default.partial_diff(partial))\n        }\n      },\n    }\n  }\n}\n\nimpl MaybeNone for BuilderConfigDiff {\n  fn is_none(&self) -> bool {\n    match self {\n      BuilderConfigDiff::Url(config) => config.is_none(),\n      BuilderConfigDiff::Server(config) => config.is_none(),\n      BuilderConfigDiff::Aws(config) => config.is_none(),\n    }\n  }\n}\n\nimpl From<PartialBuilderConfig> for BuilderConfig {\n  fn from(value: PartialBuilderConfig) -> BuilderConfig {\n    match value {\n      PartialBuilderConfig::Url(server) => {\n        BuilderConfig::Url(server.into())\n      }\n      PartialBuilderConfig::Server(server) => {\n        BuilderConfig::Server(server.into())\n      }\n      PartialBuilderConfig::Aws(builder) => {\n        BuilderConfig::Aws(builder.into())\n      }\n    }\n  }\n}\n\nimpl From<BuilderConfig> for PartialBuilderConfig {\n  fn from(value: BuilderConfig) -> Self {\n    match value {\n      BuilderConfig::Url(config) => {\n        PartialBuilderConfig::Url(config.into())\n      }\n      BuilderConfig::Server(config) => {\n        PartialBuilderConfig::Server(config.into())\n      }\n      BuilderConfig::Aws(config) => {\n        PartialBuilderConfig::Aws(config.into())\n      }\n    }\n  }\n}\n\nimpl MergePartial for BuilderConfig {\n  type Partial = PartialBuilderConfig;\n  fn merge_partial(\n    self,\n    partial: PartialBuilderConfig,\n  ) -> BuilderConfig {\n    match partial {\n      PartialBuilderConfig::Url(partial) => match self {\n        BuilderConfig::Url(config) => {\n          let config = UrlBuilderConfig {\n            address: partial.address.unwrap_or(config.address),\n            passkey: partial.passkey.unwrap_or(config.passkey),\n          };\n          BuilderConfig::Url(config)\n        }\n        _ => BuilderConfig::Url(partial.into()),\n      },\n      PartialBuilderConfig::Server(partial) => match self {\n        BuilderConfig::Server(config) => {\n          let config = ServerBuilderConfig {\n            server_id: partial.server_id.unwrap_or(config.server_id),\n          };\n          BuilderConfig::Server(config)\n        }\n        _ => BuilderConfig::Server(partial.into()),\n      },\n      PartialBuilderConfig::Aws(partial) => match self {\n        BuilderConfig::Aws(config) => {\n          let config = AwsBuilderConfig {\n            region: partial.region.unwrap_or(config.region),\n            instance_type: partial\n              .instance_type\n              .unwrap_or(config.instance_type),\n            volume_gb: partial.volume_gb.unwrap_or(config.volume_gb),\n            ami_id: partial.ami_id.unwrap_or(config.ami_id),\n            subnet_id: partial.subnet_id.unwrap_or(config.subnet_id),\n            security_group_ids: partial\n              .security_group_ids\n              .unwrap_or(config.security_group_ids),\n            key_pair_name: partial\n              .key_pair_name\n              .unwrap_or(config.key_pair_name),\n            assign_public_ip: partial\n              .assign_public_ip\n              .unwrap_or(config.assign_public_ip),\n            use_public_ip: partial\n              .use_public_ip\n              .unwrap_or(config.use_public_ip),\n            port: partial.port.unwrap_or(config.port),\n            use_https: partial.use_https.unwrap_or(config.use_https),\n            user_data: partial.user_data.unwrap_or(config.user_data),\n            git_providers: partial\n              .git_providers\n              .unwrap_or(config.git_providers),\n            docker_registries: partial\n              .docker_registries\n              .unwrap_or(config.docker_registries),\n            secrets: partial.secrets.unwrap_or(config.secrets),\n          };\n          BuilderConfig::Aws(config)\n        }\n        _ => BuilderConfig::Aws(partial.into()),\n      },\n    }\n  }\n}\n\n#[typeshare(serialized_as = \"Partial<UrlBuilderConfig>\")]\npub type _PartialUrlBuilderConfig = PartialUrlBuilderConfig;\n\n/// Configuration for a Komodo Url Builder.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[partial(skip_serializing_none, from, diff)]\npub struct UrlBuilderConfig {\n  /// The address of the Periphery agent\n  #[serde(default = \"default_address\")]\n  pub address: String,\n  /// A custom passkey to use. Otherwise, use the default passkey.\n  #[serde(default)]\n  pub passkey: String,\n}\n\nfn default_address() -> String {\n  String::from(\"https://periphery:8120\")\n}\n\nimpl Default for UrlBuilderConfig {\n  fn default() -> Self {\n    Self {\n      address: default_address(),\n      passkey: Default::default(),\n    }\n  }\n}\n\nimpl UrlBuilderConfig {\n  pub fn builder() -> UrlBuilderConfigBuilder {\n    UrlBuilderConfigBuilder::default()\n  }\n}\n\n#[typeshare(serialized_as = \"Partial<ServerBuilderConfig>\")]\npub type _PartialServerBuilderConfig = PartialServerBuilderConfig;\n\n/// Configuration for a Komodo Server Builder.\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, Builder, Partial,\n)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[partial(skip_serializing_none, from, diff)]\npub struct ServerBuilderConfig {\n  /// The server id of the builder\n  #[serde(default, alias = \"server\")]\n  #[partial_attr(serde(alias = \"server\"))]\n  pub server_id: String,\n}\n\nimpl ServerBuilderConfig {\n  pub fn builder() -> ServerBuilderConfigBuilder {\n    ServerBuilderConfigBuilder::default()\n  }\n}\n\n#[typeshare(serialized_as = \"Partial<AwsBuilderConfig>\")]\npub type _PartialAwsBuilderConfig = PartialAwsBuilderConfig;\n\n/// Configuration for an AWS builder.\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize, Builder, Partial)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[partial(skip_serializing_none, from, diff)]\npub struct AwsBuilderConfig {\n  /// The AWS region to create the instance in\n  #[serde(default = \"aws_default_region\")]\n  #[builder(default = \"aws_default_region()\")]\n  #[partial_default(aws_default_region())]\n  pub region: String,\n\n  /// The instance type to create for the build\n  #[serde(default = \"aws_default_instance_type\")]\n  #[builder(default = \"aws_default_instance_type()\")]\n  #[partial_default(aws_default_instance_type())]\n  pub instance_type: String,\n\n  /// The size of the builder volume in gb\n  #[serde(default = \"aws_default_volume_gb\")]\n  #[builder(default = \"aws_default_volume_gb()\")]\n  #[partial_default(aws_default_volume_gb())]\n  pub volume_gb: i32,\n\n  /// The port periphery will be running on.\n  /// Default: `8120`\n  #[serde(default = \"default_port\")]\n  #[builder(default = \"default_port()\")]\n  #[partial_default(default_port())]\n  pub port: i32,\n\n  #[serde(default = \"default_use_https\")]\n  #[builder(default = \"default_use_https()\")]\n  #[partial_default(default_use_https())]\n  pub use_https: bool,\n\n  /// The EC2 ami id to create.\n  /// The ami should have the periphery client configured to start on startup,\n  /// and should have the necessary github / dockerhub accounts configured.\n  #[serde(default)]\n  #[builder(default)]\n  pub ami_id: String,\n  /// The subnet id to create the instance in.\n  #[serde(default)]\n  #[builder(default)]\n  pub subnet_id: String,\n  /// The key pair name to attach to the instance\n  #[serde(default)]\n  #[builder(default)]\n  pub key_pair_name: String,\n  /// Whether to assign the instance a public IP address.\n  /// Likely needed for the instance to be able to reach the open internet.\n  #[serde(default)]\n  #[builder(default)]\n  pub assign_public_ip: bool,\n  /// Whether core should use the public IP address to communicate with periphery on the builder.\n  /// If false, core will communicate with the instance using the private IP.\n  #[serde(default)]\n  #[builder(default)]\n  pub use_public_ip: bool,\n  /// The security group ids to attach to the instance.\n  /// This should include a security group to allow core inbound access to the periphery port.\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub security_group_ids: Vec<String>,\n  /// The user data to deploy the instance with.\n  #[serde(default)]\n  #[builder(default)]\n  pub user_data: String,\n\n  /// Which git providers are available on the AMI\n  #[serde(default)]\n  #[builder(default)]\n  pub git_providers: Vec<GitProvider>,\n  /// Which docker registries are available on the AMI.\n  #[serde(default)]\n  #[builder(default)]\n  pub docker_registries: Vec<DockerRegistry>,\n  /// Which secrets are available on the AMI.\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub secrets: Vec<String>,\n}\n\nimpl Default for AwsBuilderConfig {\n  fn default() -> Self {\n    Self {\n      region: aws_default_region(),\n      instance_type: aws_default_instance_type(),\n      volume_gb: aws_default_volume_gb(),\n      port: default_port(),\n      use_https: default_use_https(),\n      ami_id: Default::default(),\n      subnet_id: Default::default(),\n      security_group_ids: Default::default(),\n      key_pair_name: Default::default(),\n      assign_public_ip: Default::default(),\n      use_public_ip: Default::default(),\n      user_data: Default::default(),\n      git_providers: Default::default(),\n      docker_registries: Default::default(),\n      secrets: Default::default(),\n    }\n  }\n}\n\nimpl AwsBuilderConfig {\n  pub fn builder() -> AwsBuilderConfigBuilder {\n    AwsBuilderConfigBuilder::default()\n  }\n}\n\nfn aws_default_region() -> String {\n  String::from(\"us-east-1\")\n}\n\nfn aws_default_instance_type() -> String {\n  String::from(\"c5.2xlarge\")\n}\n\nfn aws_default_volume_gb() -> i32 {\n  20\n}\n\nfn default_port() -> i32 {\n  8120\n}\n\nfn default_use_https() -> bool {\n  true\n}\n\n#[typeshare]\npub type BuilderQuery = ResourceQuery<BuilderQuerySpecifics>;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct BuilderQuerySpecifics {}\n\nimpl AddFilters for BuilderQuerySpecifics {}\n"
  },
  {
    "path": "client/core/rs/src/entities/config/cli/args/container.rs",
    "content": "#[derive(Debug, Clone, clap::Parser)]\npub struct Container {\n  /// Other container utilities\n  #[command(subcommand)]\n  pub command: Option<ContainerCommand>,\n  /// List all containers, including stopped ones.\n  /// This overrides 'down'.\n  #[arg(long, short = 'a', default_value_t = false)]\n  pub all: bool,\n  /// Reverse the ordering of results,\n  /// so non-running containers are listed first if --all is passed.\n  #[arg(long, short = 'r', default_value_t = false)]\n  pub reverse: bool,\n  /// List only non-running containers.\n  #[arg(long, short = 'd', default_value_t = false)]\n  pub down: bool,\n  /// Include links. Makes the table very large.\n  #[arg(long, short = 'l', default_value_t = false)]\n  pub links: bool,\n  /// Filter containers by a particular server.\n  /// Supports wildcard syntax.\n  /// Can be specified multiple times. (alias `s`)\n  #[arg(name = \"server\", long, short = 's')]\n  pub servers: Vec<String>,\n  /// Filter containers by a name. Supports wildcard syntax.\n  /// Can be specified multiple times. (alias `c`)\n  #[arg(name = \"container\", long, short = 'c')]\n  pub containers: Vec<String>,\n  /// Filter containers by image. Supports wildcard syntax.\n  /// Can be specified multiple times. (alias `i`)\n  #[arg(name = \"image\", long, short = 'i')]\n  pub images: Vec<String>,\n  /// Filter containers by image. Supports wildcard syntax.\n  /// Can be specified multiple times. (alias `--net`, `n`)\n  #[arg(name = \"network\", alias = \"net\", long, short = 'n')]\n  pub networks: Vec<String>,\n  /// Specify the format of the output.\n  #[arg(long, short = 'f', default_value_t = super::CliFormat::Table)]\n  pub format: super::CliFormat,\n}\n\n#[derive(Debug, Clone, clap::Subcommand)]\npub enum ContainerCommand {\n  /// Inspect containers\n  #[clap(alias = \"i\")]\n  Inspect(InspectContainer),\n}\n\n#[derive(Debug, Clone, clap::Parser)]\npub struct InspectContainer {\n  /// The container name. If it matches multiple containers and no server is specified,\n  /// each container's inspect info will be logged.\n  pub container: String,\n  /// Select the particular server that container is on.\n  #[arg(name = \"server\", long, short = 's')]\n  pub servers: Vec<String>,\n  /// Only show the .State part of the inspect response.\n  #[arg(long, short = 'u')]\n  pub state: bool,\n  /// Only show the .Mounts part of the inspect response.\n  #[arg(long, short = 'm')]\n  pub mounts: bool,\n  /// Only show the .HostConfig part of the inspect response.\n  #[arg(long, short = 'f')]\n  pub host_config: bool,\n  /// Only show the .Config part of the inspect response.\n  #[arg(long, short = 'c')]\n  pub config: bool,\n  /// Only show the .NetworkSettings part of the inspect response.\n  #[arg(long, short = 'n')]\n  pub network_settings: bool,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/config/cli/args/database.rs",
    "content": "use std::path::PathBuf;\n\n#[derive(Debug, Clone, clap::Subcommand)]\npub enum DatabaseCommand {\n  /// Triggers database backup to compressed files\n  /// organized by time the backup was taken. (alias: `bkp`)\n  #[clap(alias = \"bkp\")]\n  Backup {\n    /// Optionally provide a specific backups folder.\n    /// Default: `/backups`\n    #[arg(long, short = 'f')]\n    backups_folder: Option<PathBuf>,\n    /// Always continue on user confirmation prompts.\n    #[arg(long, short = 'y', default_value_t = false)]\n    yes: bool,\n  },\n  /// Restores the database from backup files. (alias: `rst`)\n  #[clap(alias = \"rst\")]\n  Restore {\n    /// Optionally provide a specific backups folder.\n    /// Default: `/backups`\n    #[arg(long, short = 'f')]\n    backups_folder: Option<PathBuf>,\n    /// Optionally provide a specific restore folder.\n    /// If not provided, will use the most recent backup folder.\n    ///\n    /// Example: `2025-08-01_05-04-53`\n    #[arg(long, short = 'r')]\n    restore_folder: Option<PathBuf>,\n    /// Whether to index the target database. Default: true\n    #[arg(long, short = 'i', default_value_t = true)]\n    index: bool,\n    /// Always continue on user confirmation prompts.\n    #[arg(long, short = 'y', default_value_t = false)]\n    yes: bool,\n  },\n  /// Prunes database backups if there are greater than\n  /// the configured `max_backups` (KOMODO_CLI_MAX_BACKUPS).\n  Prune {\n    /// Optionally provide a specific backups folder.\n    /// Default: `/backups`\n    #[arg(long, short = 'f')]\n    backups_folder: Option<PathBuf>,\n    /// Always continue on user confirmation prompts.\n    #[arg(long, short = 'y', default_value_t = false)]\n    yes: bool,\n  },\n  /// Copy the database to another running database. (alias: `cp`)\n  #[clap(alias = \"cp\")]\n  Copy {\n    /// The target database uri to copy to.\n    #[arg(long)]\n    uri: Option<String>,\n    /// The target database address to copy to\n    #[arg(long, short = 'a')]\n    address: Option<String>,\n    /// The target database username\n    #[arg(long, short = 'u')]\n    username: Option<String>,\n    /// The target database password\n    #[arg(long, short = 'p')]\n    password: Option<String>,\n    /// The target db name to copy to.\n    #[arg(long, short = 'd')]\n    db_name: Option<String>,\n    /// Whether to index the target database. Default: true\n    #[arg(long, short = 'i', default_value_t = true)]\n    index: bool,\n    /// Always continue on user confirmation prompts.\n    #[arg(long, short = 'y', default_value_t = false)]\n    yes: bool,\n  },\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/config/cli/args/list.rs",
    "content": "use crate::entities::resource::TemplatesQueryBehavior;\n\n#[derive(Debug, Clone, clap::Parser)]\npub struct List {\n  /// List specific resources\n  #[command(subcommand)]\n  pub command: Option<ListCommand>,\n  /// List all resources, including down ones.\n  #[arg(long, short = 'a', default_value_t = false)]\n  pub all: bool,\n  /// Reverse the ordering of results,\n  /// so non-running containers are listed first if --all is passed.\n  #[arg(long, short = 'r', default_value_t = false)]\n  pub reverse: bool,\n  /// List only non-running / non-ok resources.\n  #[arg(long, short = 'd', default_value_t = false)]\n  pub down: bool,\n  /// List only \"in progress\" / \"pending\" resources, like Actions / Procedures that are running (alias: `pending`)\n  #[arg(\n    long,\n    short = 'p',\n    alias = \"pending\",\n    default_value_t = false\n  )]\n  pub in_progress: bool,\n  /// Include links. Makes the table very large.\n  #[arg(long, short = 'l', default_value_t = false)]\n  pub links: bool,\n  /// Whether to include resources marked as templates in results. Default: 'exclude'.\n  #[arg(\n    long,\n    short = 'm',\n    default_value_t = TemplatesQueryBehavior::Exclude,\n  )]\n  pub templates: TemplatesQueryBehavior,\n  /// Filter by a particular name. Supports wildcard.\n  /// Can be specified multiple times. (alias `n`)\n  #[arg(name = \"name\", long, short = 'n')]\n  pub names: Vec<String>,\n  /// Filter by a particular tag.\n  /// Can be specified multiple times. (alias `t`)\n  #[arg(name = \"tag\", long, short = 't')]\n  pub tags: Vec<String>,\n  /// Filter by a particular server. Supports wildcard.\n  /// Can be specified multiple times. (alias `s`)\n  #[arg(name = \"server\", long, short = 's')]\n  pub servers: Vec<String>,\n  /// Filter by a particular builder. Supports wildcard.\n  /// Can be specified multiple times. (alias `b`)\n  #[arg(name = \"builder\", long, short = 'b')]\n  pub builders: Vec<String>,\n  /// Specify the format of the output.\n  #[arg(long, short = 'f', default_value_t = super::CliFormat::Table)]\n  pub format: super::CliFormat,\n}\n\nimpl From<List> for ResourceFilters {\n  fn from(value: List) -> Self {\n    Self {\n      all: value.all,\n      reverse: value.reverse,\n      down: value.down,\n      in_progress: value.in_progress,\n      links: value.links,\n      templates: value.templates,\n      names: value.names,\n      tags: value.tags,\n      servers: value.servers,\n      builders: value.builders,\n      format: value.format,\n    }\n  }\n}\n\n#[derive(Debug, Clone, clap::Subcommand)]\npub enum ListCommand {\n  /// List Servers (aliases: `server`, `sv`)\n  #[clap(alias = \"server\", alias = \"sv\")]\n  Servers(ResourceFilters),\n  /// List Stacks (aliases: `stack`, `st`)\n  #[clap(alias = \"stack\", alias = \"st\")]\n  Stacks(ResourceFilters),\n  /// List Deployments (aliases: `deployment`, `dp`)\n  #[clap(alias = \"deployment\", alias = \"dp\")]\n  Deployments(ResourceFilters),\n  /// List Builds (aliases: `build`, `bd`)\n  #[clap(alias = \"build\", alias = \"bd\")]\n  Builds(ResourceFilters),\n  /// List Repos (aliases: `repo`, `rp`)\n  #[clap(alias = \"repo\", alias = \"rp\")]\n  Repos(ResourceFilters),\n  /// List Procedures (aliases: `procedure`, `pr`)\n  #[clap(alias = \"procedure\", alias = \"pr\")]\n  Procedures(ResourceFilters),\n  /// List Actions (aliases: `action`, `ac`)\n  #[clap(alias = \"action\", alias = \"ac\")]\n  Actions(ResourceFilters),\n  /// List Syncs (aliases: `sync`, `sn`)\n  #[clap(alias = \"sync\", alias = \"sn\")]\n  Syncs(ResourceFilters),\n  /// List scheduled Procedures / Actions (aliases: `sched`, `sc`)\n  #[clap(alias = \"sched\", alias = \"sc\")]\n  Schedules(ResourceFilters),\n  /// List Builders (aliases: `builder`, `bldr`)\n  #[clap(alias = \"builder\", alias = \"bldr\")]\n  Builders(ResourceFilters),\n  /// List Alerters (aliases: `alerter`, `alrt`)\n  #[clap(alias = \"alerter\", alias = \"alrt\")]\n  Alerters(ResourceFilters),\n}\n\n#[derive(Debug, Clone, clap::Parser)]\npub struct ResourceFilters {\n  /// List all resources, including down ones.\n  #[arg(long, short = 'a', default_value_t = false)]\n  pub all: bool,\n  /// Reverse the ordering of results,\n  /// so non-running containers are listed first if --all is passed.\n  #[arg(long, short = 'r', default_value_t = false)]\n  pub reverse: bool,\n  /// List only non-running / non-ok resources.\n  #[arg(long, short = 'd', default_value_t = false)]\n  pub down: bool,\n  /// List only \"in progress\" / \"pending\" resources, like Actions / Procedures that are running (alias: `pending`)\n  #[arg(\n    long,\n    short = 'p',\n    alias = \"pending\",\n    default_value_t = false\n  )]\n  pub in_progress: bool,\n  /// Include links. Makes the table very large.\n  #[arg(long, short = 'l', default_value_t = false)]\n  pub links: bool,\n  /// Whether to include resources marked as templates in results. Default: 'exclude'.\n  #[arg(\n    long,\n    short = 'm',\n    default_value_t = TemplatesQueryBehavior::Exclude,\n  )]\n  pub templates: TemplatesQueryBehavior,\n  /// Filter by a particular name. Supports wildcard.\n  /// Can be specified multiple times. (alias `n`)\n  #[arg(name = \"name\", long, short = 'n')]\n  pub names: Vec<String>,\n  /// Filter by a particular tag.\n  /// Can be specified multiple times. (alias `t`)\n  #[arg(name = \"tag\", long, short = 't')]\n  pub tags: Vec<String>,\n  /// Filter by a particular server. Supports wildcard.\n  /// Can be specified multiple times. (alias `s`)\n  #[arg(name = \"server\", long, short = 's')]\n  pub servers: Vec<String>,\n  /// Filter by a particular builder. Supports wildcard.\n  /// Can be specified multiple times. (alias `b`)\n  #[arg(name = \"builder\", long, short = 'b')]\n  pub builders: Vec<String>,\n  /// Specify the format of the output.\n  #[arg(long, short = 'f', default_value_t = super::CliFormat::Table)]\n  pub format: super::CliFormat,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/config/cli/args/mod.rs",
    "content": "//! Module for parsing the Komodo CLI arguments\n\nuse std::path::PathBuf;\n\nuse crate::api::execute::Execution;\n\npub mod container;\npub mod database;\npub mod list;\npub mod update;\n\n#[derive(Debug, clap::Parser)]\n#[command(name = \"komodo-cli\", version, about = \"\", author)]\npub struct CliArgs {\n  /// The command to run\n  #[command(subcommand)]\n  pub command: Command,\n\n  /// Choose a custom [[profile]] name / alias set in a `komodo.cli.toml` file.\n  #[arg(long, short = 'p')]\n  pub profile: Option<String>,\n\n  /// Sets the path of a config file or directory to use.\n  /// Can use multiple times\n  #[arg(long, short = 'c')]\n  pub config_path: Option<Vec<PathBuf>>,\n\n  /// Sets the keywords to match directory cli config file names on.\n  /// Supports wildcard syntax.\n  /// Can use multiple times to match multiple patterns independently.\n  #[arg(long, short = 'm')]\n  pub config_keyword: Option<Vec<String>>,\n\n  /// Whether to debug print on configuration load (on startup)\n  #[arg(alias = \"debug\", long, short = 'd')]\n  pub debug_startup: Option<bool>,\n}\n\n#[derive(Debug, Clone, clap::Subcommand)]\npub enum Command {\n  /// Print the CLI config being used. (aliases: `cfg`, `cf`)\n  #[clap(alias = \"cfg\", alias = \"cf\")]\n  Config {\n    /// Whether to print the additional profiles picked up\n    #[arg(long, short = 'a', default_value_t = false)]\n    all_profiles: bool,\n\n    /// Whether to print unsanitized config,\n    /// including sensitive credentials.\n    #[arg(long, action)]\n    unsanitized: bool,\n  },\n\n  /// Container info (aliases: `ps`, `cn`, `containers`)\n  #[clap(alias = \"ps\", alias = \"cn\", alias = \"containers\")]\n  Container(container::Container),\n\n  /// Inspect containers (alias: `i`)\n  #[clap(alias = \"i\")]\n  Inspect(container::InspectContainer),\n\n  /// List Komodo resources (aliases: `ls`, `resources`)\n  #[clap(alias = \"ls\", alias = \"resources\")]\n  List(list::List),\n\n  /// Run Komodo executions. (aliases: `x`, `run`, `deploy`, `dep`, `send`)\n  #[clap(\n    alias = \"x\",\n    alias = \"run\",\n    alias = \"deploy\",\n    alias = \"dep\",\n    alias = \"send\"\n  )]\n  Execute(Execute),\n\n  /// Update resource configuration. (alias: `set`)\n  #[clap(alias = \"set\")]\n  Update {\n    #[command(subcommand)]\n    command: update::UpdateCommand,\n  },\n\n  /// Database utilities. (alias: `db`)\n  #[clap(alias = \"db\")]\n  Database {\n    #[command(subcommand)]\n    command: database::DatabaseCommand,\n  },\n}\n\n#[derive(Debug, Clone, clap::Parser)]\npub struct Execute {\n  /// The execution to run.\n  #[command(subcommand)]\n  pub execution: Execution,\n  /// Top priority Komodo host.\n  /// Eg. \"https://demo.komo.do\"\n  #[arg(long, short = 'a')]\n  pub host: Option<String>,\n  /// Top priority api key.\n  #[arg(long, short = 'k')]\n  pub key: Option<String>,\n  /// Top priority api secret.\n  #[arg(long, short = 's')]\n  pub secret: Option<String>,\n  /// Always continue on user confirmation prompts.\n  #[arg(long, short = 'y', default_value_t = false)]\n  pub yes: bool,\n}\n\n#[derive(\n  Debug, Clone, Copy, Default, strum::Display, clap::ValueEnum,\n)]\n#[strum(serialize_all = \"lowercase\")]\npub enum CliFormat {\n  /// Table output format. Default. (alias: `t`)\n  #[default]\n  #[clap(alias = \"t\")]\n  Table,\n  /// Json output format. (alias: `j`)\n  #[clap(alias = \"j\")]\n  Json,\n}\n\n#[derive(\n  Debug, Clone, Copy, Default, clap::ValueEnum, strum::Display,\n)]\n#[strum(serialize_all = \"lowercase\")]\npub enum CliEnabled {\n  #[default]\n  #[clap(alias = \"y\", alias = \"true\", alias = \"t\")]\n  Yes,\n  #[clap(alias = \"n\", alias = \"false\", alias = \"f\")]\n  No,\n}\n\nimpl From<CliEnabled> for bool {\n  fn from(value: CliEnabled) -> Self {\n    match value {\n      CliEnabled::Yes => true,\n      CliEnabled::No => false,\n    }\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/config/cli/args/update.rs",
    "content": "#[derive(Debug, Clone, clap::Subcommand)]\npub enum UpdateCommand {\n  /// Update a Build's configuration. (alias: `bld`)\n  #[clap(alias = \"bld\")]\n  Build(UpdateResource),\n  /// Update a Deployments's configuration. (alias: `dep`)\n  #[clap(alias = \"dep\")]\n  Deployment(UpdateResource),\n  /// Update a Repos's configuration.\n  Repo(UpdateResource),\n  /// Update a Servers's configuration. (alias: `srv`)\n  #[clap(alias = \"srv\")]\n  Server(UpdateResource),\n  /// Update a Stacks's configuration. (alias: `stk`)\n  #[clap(alias = \"stk\")]\n  Stack(UpdateResource),\n  /// Update a Syncs's configuration.\n  Sync(UpdateResource),\n  /// Update a Variable's value. (alias: `var`)\n  #[clap(alias = \"var\")]\n  Variable {\n    /// The name of the variable.\n    name: String,\n    /// The value to set variable to.\n    value: String,\n    /// Whether the value should be set to secret.\n    /// If unset, will leave the variable secret setting as-is.\n    #[arg(long, short = 's')]\n    secret: Option<bool>,\n    /// Always continue on user confirmation prompts.\n    #[arg(long, short = 'y', default_value_t = false)]\n    yes: bool,\n  },\n  /// Update a user's configuration, including assigning resetting password and assigning Super Admin\n  User {\n    /// The user to update\n    username: String,\n    #[command(subcommand)]\n    command: UpdateUserCommand,\n  },\n}\n\n#[derive(Debug, Clone, clap::Parser)]\npub struct UpdateResource {\n  /// The name / id of the Resource.\n  pub resource: String,\n  /// The update string, parsed using 'https://docs.rs/serde_qs/latest/serde_qs'.\n  ///\n  /// The fields can be found here: 'https://docs.rs/komodo_client/latest/komodo_client/entities/sync/struct.ResourceSyncConfig.html'\n  ///\n  /// Example: `km update build example-build \"branch=testing\"`\n  ///\n  /// Note. Should be enclosed in single or double quotes.\n  /// Values containing complex characters (like URLs)\n  /// will need to be url-encoded in order to be parsed correctly.\n  pub update: String,\n  /// Always continue on user confirmation prompts.\n  #[arg(long, short = 'y', default_value_t = false)]\n  pub yes: bool,\n}\n\n#[derive(Debug, Clone, clap::Subcommand)]\npub enum UpdateUserCommand {\n  /// Update the users password. Fails if user is not \"Local\" user (ie OIDC). (alias: `pw`)\n  #[clap(alias = \"pw\")]\n  Password {\n    /// The new password to use.\n    password: String,\n    /// Whether to print unsanitized config,\n    /// including sensitive credentials.\n    #[arg(long, action)]\n    unsanitized: bool,\n    /// Always continue on user confirmation prompts.\n    #[arg(long, short = 'y', default_value_t = false)]\n    yes: bool,\n  },\n  /// Un/assign super admin to user. (aliases: `supa`, `sa`)\n  #[clap(alias = \"supa\", alias = \"sa\")]\n  SuperAdmin {\n    #[clap(default_value_t = super::CliEnabled::Yes)]\n    enabled: super::CliEnabled,\n    /// Always continue on user confirmation prompts.\n    #[arg(long, short = 'y', default_value_t = false)]\n    yes: bool,\n  },\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/config/cli/mod.rs",
    "content": "use std::{path::PathBuf, str::FromStr};\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::{\n  deserializers::string_list_deserializer,\n  entities::{\n    config::{DatabaseConfig, empty_or_redacted},\n    logger::{LogConfig, LogLevel, StdioLogMode},\n  },\n};\n\npub mod args;\n\n/// # Komodo CLI Environment Variables\n///\n///\n#[derive(Debug, Clone, Deserialize)]\npub struct Env {\n  // ============\n  // Cli specific\n  // ============\n  /// Specify the config paths (files or folders) used to build up the\n  /// final [CliConfig].\n  /// If not provided, will use \".\" (the current working directory).\n  ///\n  /// Note. This is overridden if the equivalent arg is passed in [CliArgs].\n  #[serde(\n    default = \"default_config_paths\",\n    alias = \"komodo_cli_config_path\"\n  )]\n  pub komodo_cli_config_paths: Vec<PathBuf>,\n  /// If specifying folders, use this to narrow down which\n  /// files will be matched to parse into the final [CliConfig].\n  /// Only files inside the folders which have names containing all keywords\n  /// provided to `config_keywords` will be included.\n  ///\n  /// Note. This is overridden if the equivalent arg is passed in [CliArgs].\n  #[serde(\n    default = \"default_config_keywords\",\n    alias = \"komodo_cli_config_keyword\"\n  )]\n  pub komodo_cli_config_keywords: Vec<String>,\n  /// Will merge nested config object (eg. database) across multiple\n  /// config files. Default: `true`\n  ///\n  /// Note. This is overridden if the equivalent arg is passed in [CliArgs].\n  #[serde(default = \"super::default_merge_nested_config\")]\n  pub komodo_cli_merge_nested_config: bool,\n  /// Will extend config arrays (eg profiles) across multiple config files.\n  /// Default: `true`\n  ///\n  /// Note. This is overridden if the equivalent arg is passed in [CliArgs].\n  #[serde(default = \"super::default_extend_config_arrays\")]\n  pub komodo_cli_extend_config_arrays: bool,\n  /// Extra logs during cli config load.\n  #[serde(default)]\n  pub komodo_cli_debug_startup: bool,\n  // Override `default_profile`.\n  pub komodo_cli_default_profile: Option<String>,\n  /// Override `host` and `KOMODO_HOST`.\n  pub komodo_cli_host: Option<String>,\n  /// Override `cli_key`\n  pub komodo_cli_key: Option<String>,\n  /// Override `cli_secret`\n  pub komodo_cli_secret: Option<String>,\n  /// Override `table_borders`\n  pub komodo_cli_table_borders: Option<CliTableBorders>,\n  /// Override `backups_folder`\n  pub komodo_cli_backups_folder: Option<PathBuf>,\n  /// Override `max_backups`\n  pub komodo_cli_max_backups: Option<u16>,\n\n  /// Override `database_target_uri`\n  #[serde(alias = \"komodo_cli_database_copy_uri\")]\n  pub komodo_cli_database_target_uri: Option<String>,\n  /// Override `database_target_address`\n  #[serde(alias = \"komodo_cli_database_copy_address\")]\n  pub komodo_cli_database_target_address: Option<String>,\n  /// Override `database_target_username`\n  #[serde(alias = \"komodo_cli_database_copy_username\")]\n  pub komodo_cli_database_target_username: Option<String>,\n  /// Override `database_target_password`\n  #[serde(alias = \"komodo_cli_database_copy_password\")]\n  pub komodo_cli_database_target_password: Option<String>,\n  /// Override `database_target_db_name`\n  #[serde(alias = \"komodo_cli_database_copy_db_name\")]\n  pub komodo_cli_database_target_db_name: Option<String>,\n\n  // LOGGING\n  /// Override `logging.level`\n  pub komodo_cli_logging_level: Option<LogLevel>,\n  /// Override `logging.stdio`\n  pub komodo_cli_logging_stdio: Option<StdioLogMode>,\n  /// Override `logging.pretty`\n  pub komodo_cli_logging_pretty: Option<bool>,\n  /// Override `logging.otlp_endpoint`\n  pub komodo_cli_logging_otlp_endpoint: Option<String>,\n  /// Override `logging.opentelemetry_service_name`\n  pub komodo_cli_logging_opentelemetry_service_name: Option<String>,\n  /// Override `pretty_startup_config`\n  pub komodo_cli_pretty_startup_config: Option<bool>,\n\n  // ================\n  // Same as Core env\n  // ================\n  /// Override `host`\n  pub komodo_host: Option<String>,\n\n  // DATABASE\n  /// Override `database.uri`\n  #[serde(alias = \"komodo_mongo_uri\")]\n  pub komodo_database_uri: Option<String>,\n  /// Override `database.uri` from file\n  #[serde(alias = \"komodo_mongo_uri_file\")]\n  pub komodo_database_uri_file: Option<PathBuf>,\n  /// Override `database.address`\n  #[serde(alias = \"komodo_mongo_address\")]\n  pub komodo_database_address: Option<String>,\n  /// Override `database.username`\n  #[serde(alias = \"komodo_mongo_username\")]\n  pub komodo_database_username: Option<String>,\n  /// Override `database.username` with file\n  #[serde(alias = \"komodo_mongo_username_file\")]\n  pub komodo_database_username_file: Option<PathBuf>,\n  /// Override `database.password`\n  #[serde(alias = \"komodo_mongo_password\")]\n  pub komodo_database_password: Option<String>,\n  /// Override `database.password` with file\n  #[serde(alias = \"komodo_mongo_password_file\")]\n  pub komodo_database_password_file: Option<PathBuf>,\n  /// Override `database.db_name`\n  #[serde(alias = \"komodo_mongo_db_name\")]\n  pub komodo_database_db_name: Option<String>,\n}\n\nfn default_config_paths() -> Vec<PathBuf> {\n  if let Ok(home) = std::env::var(\"HOME\") {\n    vec![\n      PathBuf::from_str(&home).unwrap().join(\".config/komodo\"),\n      PathBuf::from_str(\".\").unwrap(),\n    ]\n  } else {\n    vec![PathBuf::from_str(\".\").unwrap()]\n  }\n}\n\nfn default_config_keywords() -> Vec<String> {\n  vec![String::from(\"*komodo.cli*.*\")]\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CliConfig {\n  /// Optional. Only relevant for top level CLI config.\n  /// Set a default profile to be used when none is provided.\n  /// This allows for quick switching between profiles while\n  /// not having to explicitly pass `-p profile`.\n  #[serde(\n    alias = \"default\",\n    skip_serializing_if = \"Option::is_none\"\n  )]\n  pub default_profile: Option<String>,\n  /// Optional. The profile name. (alias: `name`)\n  /// Configure profiles with name in the komodo.cli.toml,\n  /// and select them using `km -p profile-name ...`.\n  #[serde(\n    default,\n    alias = \"name\",\n    skip_serializing_if = \"String::is_empty\"\n  )]\n  pub config_profile: String,\n  /// Optional. The profile aliases. (aliases: `aliases`, `alias`)\n  /// Configure profiles with alias in the komodo.cli.toml,\n  /// and select them using `km -p alias ...`.\n  #[serde(\n    default,\n    alias = \"aliases\",\n    alias = \"alias\",\n    deserialize_with = \"string_list_deserializer\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub config_aliases: Vec<String>,\n  // Same as Core\n  /// The host Komodo url.\n  /// Eg. \"https://demo.komo.do\"\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub host: String,\n  /// The api key for the CLI to use\n  #[serde(alias = \"key\", skip_serializing_if = \"Option::is_none\")]\n  pub cli_key: Option<String>,\n  /// The api secret for the CLI to use\n  #[serde(alias = \"secret\", skip_serializing_if = \"Option::is_none\")]\n  pub cli_secret: Option<String>,\n  /// The format for the tables.\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub table_borders: Option<CliTableBorders>,\n  /// The root backups folder.\n  ///\n  /// Default: `/backups`.\n  ///\n  /// Backups will be created in timestamped folders eg\n  /// `/backups/2025-08-04_05_05_53`\n  #[serde(default = \"default_backups_folder\")]\n  pub backups_folder: PathBuf,\n\n  /// Specify the maximum number of backups to keep,\n  /// or 0 to disable backup pruning.\n  /// Default: `14`\n  ///\n  /// After every backup, the CLI will prune the oldest backups\n  /// if there are more backups than `max_backups`\n  #[serde(default = \"default_max_backups\")]\n  pub max_backups: u16,\n  // Same as Core\n  /// Configure database connection\n  #[serde(\n    default = \"default_database_config\",\n    alias = \"mongo\",\n    skip_serializing_if = \"database_config_is_default\"\n  )]\n  pub database: DatabaseConfig,\n  /// Configure restore / copy database connection\n  #[serde(\n    default = \"default_database_config\",\n    alias = \"database_copy\",\n    skip_serializing_if = \"database_config_is_default\"\n  )]\n  pub database_target: DatabaseConfig,\n  /// Logging configuration\n  #[serde(\n    default = \"default_log_config\",\n    skip_serializing_if = \"log_config_is_default\"\n  )]\n  pub cli_logging: LogConfig,\n  /// Configure additional profiles.\n  #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n  pub profile: Vec<CliConfig>,\n}\n\nfn default_backups_folder() -> PathBuf {\n  // SAFE: /backups is a valid path.\n  PathBuf::from_str(\"/backups\").unwrap()\n}\n\nfn default_max_backups() -> u16 {\n  14\n}\n\nfn default_database_config() -> DatabaseConfig {\n  DatabaseConfig {\n    app_name: String::from(\"komodo_cli\"),\n    ..Default::default()\n  }\n}\n\nfn database_config_is_default(db_config: &DatabaseConfig) -> bool {\n  db_config == &default_database_config()\n}\n\nfn default_log_config() -> LogConfig {\n  LogConfig {\n    location: false,\n    ..Default::default()\n  }\n}\n\nfn log_config_is_default(log_config: &LogConfig) -> bool {\n  log_config == &default_log_config()\n}\n\nimpl Default for CliConfig {\n  fn default() -> Self {\n    Self {\n      default_profile: Default::default(),\n      config_profile: Default::default(),\n      config_aliases: Default::default(),\n      cli_key: Default::default(),\n      cli_secret: Default::default(),\n      cli_logging: default_log_config(),\n      table_borders: Default::default(),\n      backups_folder: default_backups_folder(),\n      max_backups: default_max_backups(),\n      database: default_database_config(),\n      database_target: default_database_config(),\n      host: Default::default(),\n      profile: Default::default(),\n    }\n  }\n}\n\nimpl CliConfig {\n  pub fn sanitized(&self) -> CliConfig {\n    CliConfig {\n      default_profile: self.default_profile.clone(),\n      config_profile: self.config_profile.clone(),\n      config_aliases: self.config_aliases.clone(),\n      cli_key: self\n        .cli_key\n        .as_ref()\n        .map(|cli_key| empty_or_redacted(cli_key)),\n      cli_secret: self\n        .cli_secret\n        .as_ref()\n        .map(|cli_secret| empty_or_redacted(cli_secret)),\n      cli_logging: self.cli_logging.clone(),\n      table_borders: self.table_borders,\n      backups_folder: self.backups_folder.clone(),\n      max_backups: self.max_backups,\n      database_target: self.database_target.sanitized(),\n      host: self.host.clone(),\n      database: self.database.sanitized(),\n      profile: self\n        .profile\n        .iter()\n        .map(CliConfig::sanitized)\n        .collect(),\n    }\n  }\n}\n\n#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]\npub enum CliTableBorders {\n  /// Only horizontal borders. Default.\n  #[default]\n  Horizontal,\n  /// Only vertical borders.\n  Vertical,\n  /// Only borders around the outside of the table.\n  Outside,\n  /// Only borders horizontally / vertically between the rows / columns.\n  Inside,\n  /// All borders\n  All,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/config/core.rs",
    "content": "//! # Configuring the Komodo Core API\n//!\n//! Komodo Core is configured by parsing base configuration file ([CoreConfig]), and overriding\n//! any fields given in the file with ones provided on the environment ([Env]).\n//!\n//! The recommended method for running Komodo Core is via the docker image. This image has a default\n//! configuration file provided in the image, meaning any custom configuration can be provided\n//! on the environment alone. However, if a custom configuration file is prefered, it can be mounted\n//! into the image at `/config/config.toml`.\n//!\n\nuse std::{collections::HashMap, path::PathBuf, str::FromStr};\n\nuse serde::Deserialize;\n\nuse crate::entities::{\n  Timelength,\n  config::DatabaseConfig,\n  logger::{LogConfig, LogLevel, StdioLogMode},\n};\n\nuse super::{DockerRegistry, GitProvider, empty_or_redacted};\n\n/// # Komodo Core Environment Variables\n///\n/// You can override any fields of the [CoreConfig] by passing the associated\n/// environment variable. The variables should be passed in the traditional `UPPER_SNAKE_CASE` format,\n/// although the lower case format can still be parsed.\n///\n/// *Note.* The Komodo Core docker image includes the default core configuration found at\n/// [https://github.com/moghtech/komodo/blob/main/config/core.config.toml](https://github.com/moghtech/komodo/blob/main/config/core.config.toml).\n/// To configure the core api, you can either mount your own custom configuration file to\n/// `/config/config.toml` inside the container,\n/// or simply override whichever fields you need using the environment.\n#[derive(Debug, Clone, Deserialize)]\npub struct Env {\n  /// Specify a custom config path for the core config toml.\n  /// Default: `/config/config.toml`\n  #[serde(\n    default = \"default_core_config_paths\",\n    alias = \"komodo_config_path\"\n  )]\n  pub komodo_config_paths: Vec<PathBuf>,\n  /// If specifying folders, use this to narrow down which\n  /// files will be matched to parse into the final [PeripheryConfig].\n  /// Only files inside the folders which have names containing a keywords\n  /// provided to `config_keywords` will be included.\n  /// Keywords support wildcard matching syntax.\n  #[serde(\n    default = \"super::default_config_keywords\",\n    alias = \"komodo_config_keyword\"\n  )]\n  pub komodo_config_keywords: Vec<String>,\n  /// Will merge nested config object (eg. secrets, providers) across multiple\n  /// config files. Default: `true`\n  #[serde(default = \"super::default_merge_nested_config\")]\n  pub komodo_merge_nested_config: bool,\n  /// Will extend config arrays across multiple config files.\n  /// Default: `true`\n  #[serde(default = \"super::default_extend_config_arrays\")]\n  pub komodo_extend_config_arrays: bool,\n  /// Print some extra logs on startup to debug config loading issues.\n  #[serde(default)]\n  pub komodo_config_debug: bool,\n\n  /// Override `title`\n  pub komodo_title: Option<String>,\n  /// Override `host`\n  pub komodo_host: Option<String>,\n  /// Override `port`\n  pub komodo_port: Option<u16>,\n  /// Override `bind_ip`\n  pub komodo_bind_ip: Option<String>,\n  /// Override `passkey`\n  pub komodo_passkey: Option<String>,\n  /// Override `passkey` with file\n  pub komodo_passkey_file: Option<PathBuf>,\n  /// Override `timezone`\n  #[serde(alias = \"tz\", alias = \"TZ\")]\n  pub komodo_timezone: Option<String>,\n  /// Override `first_server`\n  pub komodo_first_server: Option<String>,\n  /// Override `first_server_name`\n  pub komodo_first_server_name: Option<String>,\n  /// Override `frontend_path`\n  pub komodo_frontend_path: Option<String>,\n  /// Override `jwt_secret`\n  pub komodo_jwt_secret: Option<String>,\n  /// Override `jwt_secret` from file\n  pub komodo_jwt_secret_file: Option<PathBuf>,\n  /// Override `jwt_ttl`\n  pub komodo_jwt_ttl: Option<Timelength>,\n  /// Override `sync_directory`\n  pub komodo_sync_directory: Option<PathBuf>,\n  /// Override `repo_directory`\n  pub komodo_repo_directory: Option<PathBuf>,\n  /// Override `action_directory`\n  pub komodo_action_directory: Option<PathBuf>,\n  /// Override `resource_poll_interval`\n  pub komodo_resource_poll_interval: Option<Timelength>,\n  /// Override `monitoring_interval`\n  pub komodo_monitoring_interval: Option<Timelength>,\n  /// Override `keep_stats_for_days`\n  pub komodo_keep_stats_for_days: Option<u64>,\n  /// Override `keep_alerts_for_days`\n  pub komodo_keep_alerts_for_days: Option<u64>,\n  /// Override `webhook_secret`\n  pub komodo_webhook_secret: Option<String>,\n  /// Override `webhook_secret` with file\n  pub komodo_webhook_secret_file: Option<PathBuf>,\n  /// Override `webhook_base_url`\n  pub komodo_webhook_base_url: Option<String>,\n\n  /// Override `logging.level`\n  pub komodo_logging_level: Option<LogLevel>,\n  /// Override `logging.stdio`\n  pub komodo_logging_stdio: Option<StdioLogMode>,\n  /// Override `logging.pretty`\n  pub komodo_logging_pretty: Option<bool>,\n  /// Override `logging.location`\n  pub komodo_logging_location: Option<bool>,\n  /// Override `logging.otlp_endpoint`\n  pub komodo_logging_otlp_endpoint: Option<String>,\n  /// Override `logging.opentelemetry_service_name`\n  pub komodo_logging_opentelemetry_service_name: Option<String>,\n  /// Override `pretty_startup_config`\n  pub komodo_pretty_startup_config: Option<bool>,\n  /// Override `unsafe_unsanitized_startup_config`\n  pub komodo_unsafe_unsanitized_startup_config: Option<bool>,\n\n  /// Override `transparent_mode`\n  pub komodo_transparent_mode: Option<bool>,\n  /// Override `ui_write_disabled`\n  pub komodo_ui_write_disabled: Option<bool>,\n  /// Override `enable_new_users`\n  pub komodo_enable_new_users: Option<bool>,\n  /// Override `disable_user_registration`\n  pub komodo_disable_user_registration: Option<bool>,\n  /// Override `lock_login_credentials_for`\n  pub komodo_lock_login_credentials_for: Option<Vec<String>>,\n  /// Override `disable_confirm_dialog`\n  pub komodo_disable_confirm_dialog: Option<bool>,\n  /// Override `disable_non_admin_create`\n  pub komodo_disable_non_admin_create: Option<bool>,\n  /// Override `disable_websocket_reconnect`\n  pub komodo_disable_websocket_reconnect: Option<bool>,\n  /// Override `disable_init_resources`\n  pub komodo_disable_init_resources: Option<bool>,\n  /// Override `enable_fancy_toml`\n  pub komodo_enable_fancy_toml: Option<bool>,\n\n  /// Override `local_auth`\n  pub komodo_local_auth: Option<bool>,\n  /// Override `init_admin_username`\n  pub komodo_init_admin_username: Option<String>,\n  /// Override `init_admin_username` from file\n  pub komodo_init_admin_username_file: Option<PathBuf>,\n  /// Override `init_admin_password`\n  pub komodo_init_admin_password: Option<String>,\n  /// Override `init_admin_password` from file\n  pub komodo_init_admin_password_file: Option<PathBuf>,\n\n  /// Override `oidc_enabled`\n  pub komodo_oidc_enabled: Option<bool>,\n  /// Override `oidc_provider`\n  pub komodo_oidc_provider: Option<String>,\n  /// Override `oidc_redirect_host`\n  pub komodo_oidc_redirect_host: Option<String>,\n  /// Override `oidc_client_id`\n  pub komodo_oidc_client_id: Option<String>,\n  /// Override `oidc_client_id` from file\n  pub komodo_oidc_client_id_file: Option<PathBuf>,\n  /// Override `oidc_client_secret`\n  pub komodo_oidc_client_secret: Option<String>,\n  /// Override `oidc_client_secret` from file\n  pub komodo_oidc_client_secret_file: Option<PathBuf>,\n  /// Override `oidc_use_full_email`\n  pub komodo_oidc_use_full_email: Option<bool>,\n  /// Override `oidc_additional_audiences`\n  pub komodo_oidc_additional_audiences: Option<Vec<String>>,\n  /// Override `oidc_additional_audiences` from file\n  pub komodo_oidc_additional_audiences_file: Option<PathBuf>,\n\n  /// Override `google_oauth.enabled`\n  pub komodo_google_oauth_enabled: Option<bool>,\n  /// Override `google_oauth.id`\n  pub komodo_google_oauth_id: Option<String>,\n  /// Override `google_oauth.id` from file\n  pub komodo_google_oauth_id_file: Option<PathBuf>,\n  /// Override `google_oauth.secret`\n  pub komodo_google_oauth_secret: Option<String>,\n  /// Override `google_oauth.secret` from file\n  pub komodo_google_oauth_secret_file: Option<PathBuf>,\n\n  /// Override `github_oauth.enabled`\n  pub komodo_github_oauth_enabled: Option<bool>,\n  /// Override `github_oauth.id`\n  pub komodo_github_oauth_id: Option<String>,\n  /// Override `github_oauth.id` from file\n  pub komodo_github_oauth_id_file: Option<PathBuf>,\n  /// Override `github_oauth.secret`\n  pub komodo_github_oauth_secret: Option<String>,\n  /// Override `github_oauth.secret` from file\n  pub komodo_github_oauth_secret_file: Option<PathBuf>,\n\n  /// Override `github_webhook_app.app_id`\n  pub komodo_github_webhook_app_app_id: Option<i64>,\n  /// Override `github_webhook_app.app_id` from file\n  pub komodo_github_webhook_app_app_id_file: Option<PathBuf>,\n  /// Override `github_webhook_app.installations[i].id`. Accepts comma seperated list.\n  ///\n  /// Note. Paired by index with values in `komodo_github_webhook_app_installations_namespaces`\n  pub komodo_github_webhook_app_installations_ids: Option<Vec<i64>>,\n  /// Override `github_webhook_app.installations[i].id` from file\n  pub komodo_github_webhook_app_installations_ids_file:\n    Option<PathBuf>,\n  /// Override `github_webhook_app.installations[i].namespace`. Accepts comma seperated list.\n  ///\n  /// Note. Paired by index with values in `komodo_github_webhook_app_installations_ids`\n  pub komodo_github_webhook_app_installations_namespaces:\n    Option<Vec<String>>,\n  /// Override `github_webhook_app.pk_path`\n  pub komodo_github_webhook_app_pk_path: Option<String>,\n\n  /// Override `database.uri`\n  #[serde(alias = \"komodo_mongo_uri\")]\n  pub komodo_database_uri: Option<String>,\n  /// Override `database.uri` from file\n  #[serde(alias = \"komodo_mongo_uri_file\")]\n  pub komodo_database_uri_file: Option<PathBuf>,\n  /// Override `database.address`\n  #[serde(alias = \"komodo_mongo_address\")]\n  pub komodo_database_address: Option<String>,\n  /// Override `database.username`\n  #[serde(alias = \"komodo_mongo_username\")]\n  pub komodo_database_username: Option<String>,\n  /// Override `database.username` with file\n  #[serde(alias = \"komodo_mongo_username_file\")]\n  pub komodo_database_username_file: Option<PathBuf>,\n  /// Override `database.password`\n  #[serde(alias = \"komodo_mongo_password\")]\n  pub komodo_database_password: Option<String>,\n  /// Override `database.password` with file\n  #[serde(alias = \"komodo_mongo_password_file\")]\n  pub komodo_database_password_file: Option<PathBuf>,\n  /// Override `database.app_name`\n  #[serde(alias = \"komodo_mongo_app_name\")]\n  pub komodo_database_app_name: Option<String>,\n  /// Override `database.db_name`\n  #[serde(alias = \"komodo_mongo_db_name\")]\n  pub komodo_database_db_name: Option<String>,\n\n  /// Override `aws.access_key_id`\n  pub komodo_aws_access_key_id: Option<String>,\n  /// Override `aws.access_key_id` with file\n  pub komodo_aws_access_key_id_file: Option<PathBuf>,\n  /// Override `aws.secret_access_key`\n  pub komodo_aws_secret_access_key: Option<String>,\n  /// Override `aws.secret_access_key` with file\n  pub komodo_aws_secret_access_key_file: Option<PathBuf>,\n\n  /// Override `internet_interface`\n  pub komodo_internet_interface: Option<String>,\n\n  /// Override `ssl_enabled`.\n  pub komodo_ssl_enabled: Option<bool>,\n  /// Override `ssl_key_file`\n  pub komodo_ssl_key_file: Option<PathBuf>,\n  /// Override `ssl_cert_file`\n  pub komodo_ssl_cert_file: Option<PathBuf>,\n}\n\nfn default_core_config_paths() -> Vec<PathBuf> {\n  vec![PathBuf::from_str(\"/config\").unwrap()]\n}\n\n/// # Core Configuration File\n///\n/// The Core API initializes it's configuration by reading the environment,\n/// parsing the [CoreConfig] schema from the file path specified by `env.komodo_config_path`,\n/// and then applying any config field overrides specified in the environment.\n///\n/// *Note.* The Komodo Core docker image includes the default core configuration found at\n/// [https://github.com/moghtech/komodo/blob/main/config/core.config.toml](https://github.com/moghtech/komodo/blob/main/config/core.config.toml).\n/// To configure the core api, you can either mount your own custom configuration file to\n/// `/config/config.toml` inside the container,\n/// or simply override whichever fields you need using the environment.\n///\n/// Refer to the [example file](https://github.com/moghtech/komodo/blob/main/config/core.config.toml) for a full example.\n#[derive(Debug, Clone, Deserialize)]\npub struct CoreConfig {\n  // ===========\n  // = General =\n  // ===========\n  /// The title of this Komodo Core deployment. Will be used in the browser page title.\n  /// Default: 'Komodo'\n  #[serde(default = \"default_title\")]\n  pub title: String,\n\n  /// The host to use with oauth redirect url, whatever host\n  /// the user hits to access Komodo. eg `https://komodo.domain.com`.\n  /// Only used if oauth used without user specifying redirect url themselves.\n  #[serde(default = \"default_host\")]\n  pub host: String,\n\n  /// Port the core web server runs on.\n  /// Default: 9120.\n  #[serde(default = \"default_core_port\")]\n  pub port: u16,\n\n  /// IP address the core server binds to.\n  /// Default: [::].\n  #[serde(default = \"default_core_bind_ip\")]\n  pub bind_ip: String,\n\n  /// Interface to use as default route in multi-NIC environments.\n  #[serde(default)]\n  pub internet_interface: String,\n\n  /// Sent in auth header with req to periphery.\n  /// Should be some secure hash, maybe 20-40 chars.\n  #[serde(default = \"default_passkey\")]\n  pub passkey: String,\n\n  /// A TZ Identifier. If not provided, will use Core local timezone.\n  /// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n  /// This will be populated by TZ env variable in addition to KOMODO_TIMEZONE.\n  #[serde(default)]\n  pub timezone: String,\n\n  /// Disable user ability to use the UI to update resource configuration.\n  #[serde(default)]\n  pub ui_write_disabled: bool,\n\n  /// Disable the popup confirm dialogs. All buttons will just be double click.\n  #[serde(default)]\n  pub disable_confirm_dialog: bool,\n\n  /// Disable the UI websocket from automatically reconnecting.\n  #[serde(default)]\n  pub disable_websocket_reconnect: bool,\n\n  /// Disable init system resource creation on fresh Komodo launch.\n  /// These include the Backup Core Database and Global Auto Update procedures.\n  #[serde(default)]\n  pub disable_init_resources: bool,\n\n  /// Enable the fancy TOML syntax highlighting\n  #[serde(default)]\n  pub enable_fancy_toml: bool,\n\n  /// If defined, ensure an enabled first server exists at this address.\n  /// Example: `http://periphery:8120`\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub first_server: Option<String>,\n\n  /// Give the first server this name.\n  /// Default: `Local`\n  #[serde(default = \"default_first_server_name\")]\n  pub first_server_name: String,\n\n  /// The path to the built frontend folder.\n  #[serde(default = \"default_frontend_path\")]\n  pub frontend_path: String,\n\n  /// Configure database connection\n  #[serde(default, alias = \"mongo\")]\n  pub database: DatabaseConfig,\n\n  // ================\n  // = Auth / Login =\n  // ================\n  /// enable login with local auth\n  #[serde(default)]\n  pub local_auth: bool,\n\n  /// Upon fresh launch, initalize an Admin user with this username.\n  /// If this is not provided, no initial user will be created.\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub init_admin_username: Option<String>,\n  /// Upon fresh launch, initalize an Admin user with this password.\n  /// Default: `changeme`\n  #[serde(default = \"default_init_admin_password\")]\n  pub init_admin_password: String,\n\n  /// Enable transparent mode, which gives all (enabled) users read access to all resources.\n  #[serde(default)]\n  pub transparent_mode: bool,\n\n  /// New users will be automatically enabled.\n  /// Combined with transparent mode, this is suitable for a demo instance.\n  #[serde(default)]\n  pub enable_new_users: bool,\n\n  /// Normally new users will be registered, but not enabled until an Admin enables them.\n  /// With `disable_user_registration = true`, only the first user to log in will registered as a user.\n  #[serde(default)]\n  pub disable_user_registration: bool,\n\n  /// List of usernames for which the update username / password\n  /// APIs are disabled. Used by demo to lock the 'demo' : 'demo' login.\n  ///\n  /// To lock the api for all users, use `lock_login_credentials_for = [\"__ALL__\"]`\n  #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n  pub lock_login_credentials_for: Vec<String>,\n\n  /// Normally all users can create resources.\n  /// If `disable_non_admin_create = true`, only admins will be able to create resources.\n  #[serde(default)]\n  pub disable_non_admin_create: bool,\n\n  /// Optionally provide a specific jwt secret.\n  /// Passing nothing or an empty string will cause one to be generated.\n  /// Default: \"\" (empty string)\n  #[serde(default)]\n  pub jwt_secret: String,\n\n  /// Control how long distributed JWT remain valid for.\n  /// Default: `1-day`.\n  #[serde(default = \"default_jwt_ttl\")]\n  pub jwt_ttl: Timelength,\n\n  // ========\n  // = OIDC =\n  // ========\n  /// Enable login with configured OIDC provider.\n  #[serde(default)]\n  pub oidc_enabled: bool,\n\n  /// Configure OIDC provider address for\n  /// communcation directly with Komodo Core.\n  ///\n  /// Note. Needs to be reachable from Komodo Core.\n  ///\n  /// `https://accounts.example.internal/application/o/komodo`\n  #[serde(default)]\n  pub oidc_provider: String,\n\n  /// Configure OIDC user redirect host.\n  ///\n  /// This is the host address users are redirected to in their browser,\n  /// and may be different from `oidc_provider` host.\n  /// DO NOT include the `path` part, this must be inferred.\n  /// If not provided, the host will be the same as `oidc_provider`.\n  /// Eg. `https://accounts.example.external`\n  #[serde(default)]\n  pub oidc_redirect_host: String,\n\n  /// Set OIDC client id\n  #[serde(default)]\n  pub oidc_client_id: String,\n\n  /// Set OIDC client secret\n  #[serde(default)]\n  pub oidc_client_secret: String,\n\n  /// Use the full email for usernames.\n  /// Otherwise, the @address will be stripped,\n  /// making usernames more concise.\n  #[serde(default)]\n  pub oidc_use_full_email: bool,\n\n  /// Your OIDC provider may set additional audiences other than `client_id`,\n  /// they must be added here to make claims verification work.\n  #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n  pub oidc_additional_audiences: Vec<String>,\n\n  // =========\n  // = Oauth =\n  // =========\n  /// Configure google oauth\n  #[serde(default)]\n  pub google_oauth: OauthCredentials,\n\n  /// Configure github oauth\n  #[serde(default)]\n  pub github_oauth: OauthCredentials,\n\n  // ============\n  // = Webhooks =\n  // ============\n  /// Used to verify validity from webhooks.\n  /// Should be some secure hash maybe 20-40 chars.\n  /// It is given to git provider when configuring the webhook.\n  #[serde(default)]\n  pub webhook_secret: String,\n\n  /// Override the webhook listener base url, if None will use the address defined as 'host'.\n  /// Example: `https://webhooks.komo.do`\n  ///\n  /// This can be used if Komodo Core sits on an internal network which is\n  /// unreachable directly from the open internet.\n  /// A reverse proxy in a public network can forward webhooks to Komodo.\n  #[serde(default)]\n  pub webhook_base_url: String,\n\n  /// Configure a Github Webhook app.\n  /// Allows users to manage repo webhooks from within the Komodo UI.\n  #[serde(default)]\n  pub github_webhook_app: GithubWebhookAppConfig,\n\n  // ===========\n  // = Logging =\n  // ===========\n  /// Configure logging\n  #[serde(default)]\n  pub logging: LogConfig,\n\n  /// Pretty-log (multi-line) the startup config\n  /// for easier human readability.\n  #[serde(default)]\n  pub pretty_startup_config: bool,\n\n  /// Unsafe: logs unsanitized config on startup,\n  /// in order to verify everything is being\n  /// passed correctly.\n  #[serde(default)]\n  pub unsafe_unsanitized_startup_config: bool,\n\n  // ===========\n  // = Pruning =\n  // ===========\n  /// Number of days to keep stats, or 0 to disable pruning.\n  /// Stats older than this number of days are deleted on a daily cycle\n  /// Default: 14\n  #[serde(default = \"default_prune_days\")]\n  pub keep_stats_for_days: u64,\n\n  /// Number of days to keep alerts, or 0 to disable pruning.\n  /// Alerts older than this number of days are deleted on a daily cycle\n  /// Default: 14\n  #[serde(default = \"default_prune_days\")]\n  pub keep_alerts_for_days: u64,\n\n  // ==================\n  // = Poll Intervals =\n  // ==================\n  /// Interval at which to poll resources for any updates / automated actions.\n  /// Options: `15-sec`, `1-min`, `5-min`, `15-min`, `1-hr`\n  /// Default: `5-min`.  \n  #[serde(default = \"default_poll_interval\")]\n  pub resource_poll_interval: Timelength,\n\n  /// Interval at which to collect server stats and send any alerts.\n  /// Default: `15-sec`\n  #[serde(default = \"default_monitoring_interval\")]\n  pub monitoring_interval: Timelength,\n\n  // ===================\n  // = Cloud Providers =\n  // ===================\n  /// Configure AWS credentials to use with AWS builds / server launches.\n  #[serde(default)]\n  pub aws: AwsCredentials,\n\n  // =================\n  // = Git Providers =\n  // =================\n  /// Configure git credentials used to clone private repos.\n  /// Supports any git provider.\n  #[serde(\n    default,\n    alias = \"git_provider\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub git_providers: Vec<GitProvider>,\n\n  // ======================\n  // = Registry Providers =\n  // ======================\n  /// Configure docker credentials used to push / pull images.\n  /// Supports any docker image repository.\n  #[serde(\n    default,\n    alias = \"docker_registry\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub docker_registries: Vec<DockerRegistry>,\n\n  // ===========\n  // = Secrets =\n  // ===========\n  /// Configure core-based secrets. These will be preferentially interpolated into\n  /// values if they contain a matching secret. Otherwise, the periphery will have to have the\n  /// secret configured.\n  #[serde(default, skip_serializing_if = \"HashMap::is_empty\")]\n  pub secrets: HashMap<String, String>,\n\n  // =======\n  // = SSL =\n  // =======\n  /// Whether to enable ssl.\n  #[serde(default)]\n  pub ssl_enabled: bool,\n\n  /// Path to the ssl key.\n  /// Default: `/config/ssl/key.pem`.\n  #[serde(default = \"default_ssl_key_file\")]\n  pub ssl_key_file: PathBuf,\n\n  /// Path to the ssl cert.\n  /// Default: `/config/ssl/cert.pem`.\n  #[serde(default = \"default_ssl_cert_file\")]\n  pub ssl_cert_file: PathBuf,\n\n  // =========\n  // = Other =\n  // =========\n  /// Configure directory to store sync files.\n  /// Default: `/syncs`\n  #[serde(default = \"default_sync_directory\")]\n  pub sync_directory: PathBuf,\n\n  /// Specify the directory used to clone stack / repo / build repos, for latest hash / contents.\n  /// The default is fine when using a container.\n  /// Default: `/repo-cache`\n  #[serde(default = \"default_repo_directory\")]\n  pub repo_directory: PathBuf,\n\n  /// Specify the directory used to temporarily write typescript files used with actions.\n  /// Default: `/action-cache`\n  #[serde(default = \"default_action_directory\")]\n  pub action_directory: PathBuf,\n}\n\nfn default_title() -> String {\n  String::from(\"Komodo\")\n}\n\nfn default_host() -> String {\n  String::from(\"https://komodo.example.com\")\n}\n\nfn default_core_port() -> u16 {\n  9120\n}\n\nfn default_core_bind_ip() -> String {\n  \"[::]\".to_string()\n}\n\nfn default_passkey() -> String {\n  String::from(\"default-passkey-changeme\")\n}\n\nfn default_frontend_path() -> String {\n  \"/app/frontend\".to_string()\n}\n\nfn default_first_server_name() -> String {\n  String::from(\"Local\")\n}\n\nfn default_jwt_ttl() -> Timelength {\n  Timelength::OneDay\n}\n\nfn default_init_admin_password() -> String {\n  String::from(\"changeme\")\n}\n\nfn default_sync_directory() -> PathBuf {\n  // unwrap ok: `/syncs` will always be valid path\n  PathBuf::from_str(\"/syncs\").unwrap()\n}\n\nfn default_repo_directory() -> PathBuf {\n  // unwrap ok: `/repo-cache` will always be valid path\n  PathBuf::from_str(\"/repo-cache\").unwrap()\n}\n\nfn default_action_directory() -> PathBuf {\n  // unwrap ok: `/action-cache` will always be valid path\n  PathBuf::from_str(\"/action-cache\").unwrap()\n}\n\nfn default_prune_days() -> u64 {\n  14\n}\n\nfn default_poll_interval() -> Timelength {\n  Timelength::OneHour\n}\n\nfn default_monitoring_interval() -> Timelength {\n  Timelength::FifteenSeconds\n}\n\nfn default_ssl_key_file() -> PathBuf {\n  \"/config/ssl/key.pem\".parse().unwrap()\n}\n\nfn default_ssl_cert_file() -> PathBuf {\n  \"/config/ssl/cert.pem\".parse().unwrap()\n}\n\nimpl Default for CoreConfig {\n  fn default() -> Self {\n    Self {\n      title: default_title(),\n      host: default_host(),\n      port: default_core_port(),\n      bind_ip: default_core_bind_ip(),\n      internet_interface: Default::default(),\n      passkey: default_passkey(),\n      timezone: Default::default(),\n      ui_write_disabled: Default::default(),\n      disable_confirm_dialog: Default::default(),\n      disable_websocket_reconnect: Default::default(),\n      disable_init_resources: Default::default(),\n      enable_fancy_toml: Default::default(),\n      first_server: Default::default(),\n      first_server_name: default_first_server_name(),\n      frontend_path: default_frontend_path(),\n      database: Default::default(),\n      local_auth: Default::default(),\n      init_admin_username: Default::default(),\n      init_admin_password: default_init_admin_password(),\n      transparent_mode: Default::default(),\n      enable_new_users: Default::default(),\n      disable_user_registration: Default::default(),\n      lock_login_credentials_for: Default::default(),\n      disable_non_admin_create: Default::default(),\n      jwt_secret: Default::default(),\n      jwt_ttl: default_jwt_ttl(),\n      oidc_enabled: Default::default(),\n      oidc_provider: Default::default(),\n      oidc_redirect_host: Default::default(),\n      oidc_client_id: Default::default(),\n      oidc_client_secret: Default::default(),\n      oidc_use_full_email: Default::default(),\n      oidc_additional_audiences: Default::default(),\n      google_oauth: Default::default(),\n      github_oauth: Default::default(),\n      webhook_secret: Default::default(),\n      webhook_base_url: Default::default(),\n      github_webhook_app: Default::default(),\n      logging: Default::default(),\n      pretty_startup_config: Default::default(),\n      unsafe_unsanitized_startup_config: Default::default(),\n      keep_stats_for_days: default_prune_days(),\n      keep_alerts_for_days: default_prune_days(),\n      resource_poll_interval: default_poll_interval(),\n      monitoring_interval: default_monitoring_interval(),\n      aws: Default::default(),\n      git_providers: Default::default(),\n      docker_registries: Default::default(),\n      secrets: Default::default(),\n      ssl_enabled: Default::default(),\n      ssl_key_file: default_ssl_key_file(),\n      ssl_cert_file: default_ssl_cert_file(),\n      sync_directory: default_sync_directory(),\n      repo_directory: default_repo_directory(),\n      action_directory: default_action_directory(),\n    }\n  }\n}\n\nimpl CoreConfig {\n  pub fn sanitized(&self) -> CoreConfig {\n    let config = self.clone();\n    CoreConfig {\n      title: config.title,\n      host: config.host,\n      port: config.port,\n      bind_ip: config.bind_ip,\n      passkey: empty_or_redacted(&config.passkey),\n      timezone: config.timezone,\n      first_server: config.first_server,\n      first_server_name: config.first_server_name,\n      frontend_path: config.frontend_path,\n      jwt_secret: empty_or_redacted(&config.jwt_secret),\n      jwt_ttl: config.jwt_ttl,\n      repo_directory: config.repo_directory,\n      action_directory: config.action_directory,\n      sync_directory: config.sync_directory,\n      internet_interface: config.internet_interface,\n      resource_poll_interval: config.resource_poll_interval,\n      monitoring_interval: config.monitoring_interval,\n      keep_stats_for_days: config.keep_stats_for_days,\n      keep_alerts_for_days: config.keep_alerts_for_days,\n      logging: config.logging,\n      pretty_startup_config: config.pretty_startup_config,\n      unsafe_unsanitized_startup_config: config\n        .unsafe_unsanitized_startup_config,\n      transparent_mode: config.transparent_mode,\n      ui_write_disabled: config.ui_write_disabled,\n      disable_confirm_dialog: config.disable_confirm_dialog,\n      disable_websocket_reconnect: config.disable_websocket_reconnect,\n      disable_init_resources: config.disable_init_resources,\n      enable_fancy_toml: config.enable_fancy_toml,\n      enable_new_users: config.enable_new_users,\n      disable_user_registration: config.disable_user_registration,\n      disable_non_admin_create: config.disable_non_admin_create,\n      lock_login_credentials_for: config.lock_login_credentials_for,\n      local_auth: config.local_auth,\n      init_admin_username: config\n        .init_admin_username\n        .map(|u| empty_or_redacted(&u)),\n      init_admin_password: empty_or_redacted(\n        &config.init_admin_password,\n      ),\n      oidc_enabled: config.oidc_enabled,\n      oidc_provider: config.oidc_provider,\n      oidc_redirect_host: config.oidc_redirect_host,\n      oidc_client_id: empty_or_redacted(&config.oidc_client_id),\n      oidc_client_secret: empty_or_redacted(\n        &config.oidc_client_secret,\n      ),\n      oidc_use_full_email: config.oidc_use_full_email,\n      oidc_additional_audiences: config\n        .oidc_additional_audiences\n        .iter()\n        .map(|aud| empty_or_redacted(aud))\n        .collect(),\n      google_oauth: OauthCredentials {\n        enabled: config.google_oauth.enabled,\n        id: empty_or_redacted(&config.google_oauth.id),\n        secret: empty_or_redacted(&config.google_oauth.id),\n      },\n      github_oauth: OauthCredentials {\n        enabled: config.github_oauth.enabled,\n        id: empty_or_redacted(&config.github_oauth.id),\n        secret: empty_or_redacted(&config.github_oauth.id),\n      },\n      webhook_secret: empty_or_redacted(&config.webhook_secret),\n      webhook_base_url: config.webhook_base_url,\n      github_webhook_app: config.github_webhook_app,\n      database: config.database.sanitized(),\n      aws: AwsCredentials {\n        access_key_id: empty_or_redacted(&config.aws.access_key_id),\n        secret_access_key: empty_or_redacted(\n          &config.aws.secret_access_key,\n        ),\n      },\n      secrets: config\n        .secrets\n        .into_iter()\n        .map(|(id, secret)| (id, empty_or_redacted(&secret)))\n        .collect(),\n      git_providers: config\n        .git_providers\n        .into_iter()\n        .map(|mut provider| {\n          provider.accounts.iter_mut().for_each(|account| {\n            account.token = empty_or_redacted(&account.token);\n          });\n          provider\n        })\n        .collect(),\n      docker_registries: config\n        .docker_registries\n        .into_iter()\n        .map(|mut provider| {\n          provider.accounts.iter_mut().for_each(|account| {\n            account.token = empty_or_redacted(&account.token);\n          });\n          provider\n        })\n        .collect(),\n\n      ssl_enabled: config.ssl_enabled,\n      ssl_key_file: config.ssl_key_file,\n      ssl_cert_file: config.ssl_cert_file,\n    }\n  }\n}\n\n/// Generic Oauth credentials\n#[derive(Debug, Clone, Default, Deserialize)]\npub struct OauthCredentials {\n  /// Whether this oauth method is available for usage.\n  #[serde(default)]\n  pub enabled: bool,\n  /// The Oauth client id.\n  #[serde(default)]\n  pub id: String,\n  /// The Oauth client secret.\n  #[serde(default)]\n  pub secret: String,\n}\n\n/// Provide AWS credentials for Komodo to use.\n#[derive(Debug, Clone, Default, Deserialize)]\npub struct AwsCredentials {\n  /// The aws ACCESS_KEY_ID\n  pub access_key_id: String,\n  /// The aws SECRET_ACCESS_KEY\n  pub secret_access_key: String,\n}\n\n/// Provide configuration for a Github Webhook app.\n#[derive(Debug, Clone, Deserialize)]\npub struct GithubWebhookAppConfig {\n  /// Github app id\n  pub app_id: i64,\n  /// Configure the app installations on multiple accounts / organizations.\n  pub installations: Vec<GithubWebhookAppInstallationConfig>,\n  /// Private key path. Default: /github-private-key.pem.\n  #[serde(default = \"default_private_key_path\")]\n  pub pk_path: String,\n}\n\nfn default_private_key_path() -> String {\n  String::from(\"/github/private-key.pem\")\n}\n\nimpl Default for GithubWebhookAppConfig {\n  fn default() -> Self {\n    GithubWebhookAppConfig {\n      app_id: 0,\n      installations: Default::default(),\n      pk_path: default_private_key_path(),\n    }\n  }\n}\n\n/// Provide configuration for a Github Webhook app installation.\n#[derive(Debug, Clone, Deserialize)]\npub struct GithubWebhookAppInstallationConfig {\n  /// The installation ID\n  pub id: i64,\n  /// The user or organization name\n  pub namespace: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/config/mod.rs",
    "content": "use std::sync::OnceLock;\n\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\npub mod cli;\npub mod core;\npub mod periphery;\n\nfn default_config_keywords() -> Vec<String> {\n  vec![String::from(\"*config.*\")]\n}\n\nfn default_merge_nested_config() -> bool {\n  true\n}\n\nfn default_extend_config_arrays() -> bool {\n  true\n}\n\n/// Provide database connection information.\n/// Komodo uses the MongoDB api driver for database communication,\n/// and FerretDB to support Postgres and Sqlite storage options.\n///\n/// Must provide ONE of:\n/// 1. `uri`\n/// 2. `address` + `username` + `password`\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct DatabaseConfig {\n  /// Full mongo uri string, eg. `mongodb://username:password@your.mongo.int:27017`\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub uri: String,\n  /// Just the address part of the mongo uri, eg `your.mongo.int:27017`\n  #[serde(\n    default = \"default_database_address\",\n    skip_serializing_if = \"String::is_empty\"\n  )]\n  pub address: String,\n  /// Mongo user username\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub username: String,\n  /// Mongo user password\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub password: String,\n  /// Mongo app name. default: `komodo_core`\n  #[serde(default = \"default_database_app_name\")]\n  pub app_name: String,\n  /// Mongo db name. Which mongo database to create the collections in.\n  /// Default: `komodo`.\n  #[serde(default = \"default_database_db_name\")]\n  pub db_name: String,\n}\n\nfn default_database_address() -> String {\n  String::from(\"localhost:27017\")\n}\n\nfn default_database_app_name() -> String {\n  \"komodo_core\".to_string()\n}\n\nfn default_database_db_name() -> String {\n  \"komodo\".to_string()\n}\n\nimpl Default for DatabaseConfig {\n  fn default() -> Self {\n    Self {\n      uri: Default::default(),\n      address: default_database_address(),\n      username: Default::default(),\n      password: Default::default(),\n      app_name: default_database_app_name(),\n      db_name: default_database_db_name(),\n    }\n  }\n}\n\nfn default_database_config() -> &'static DatabaseConfig {\n  static DEFAULT_DATABASE_CONFIG: OnceLock<DatabaseConfig> =\n    OnceLock::new();\n  DEFAULT_DATABASE_CONFIG.get_or_init(Default::default)\n}\n\nimpl DatabaseConfig {\n  pub fn sanitized(&self) -> DatabaseConfig {\n    DatabaseConfig {\n      uri: empty_or_redacted(&self.uri),\n      address: self.address.clone(),\n      username: empty_or_redacted(&self.username),\n      password: empty_or_redacted(&self.password),\n      app_name: self.app_name.clone(),\n      db_name: self.db_name.clone(),\n    }\n  }\n\n  pub fn is_default(&self) -> bool {\n    self == default_database_config()\n  }\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Eq,\n  Hash,\n  PartialOrd,\n  Ord,\n  Serialize,\n  Deserialize,\n)]\npub struct GitProvider {\n  /// The git provider domain. Default: `github.com`.\n  #[serde(default = \"default_git_provider\")]\n  pub domain: String,\n  /// Whether to use https. Default: true.\n  #[serde(default = \"default_git_https\")]\n  pub https: bool,\n  /// The accounts on the git provider. Required.\n  #[serde(alias = \"account\")]\n  pub accounts: Vec<ProviderAccount>,\n}\n\nfn default_git_provider() -> String {\n  String::from(\"github.com\")\n}\n\nfn default_git_https() -> bool {\n  true\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Eq,\n  Hash,\n  PartialOrd,\n  Ord,\n  Serialize,\n  Deserialize,\n)]\npub struct DockerRegistry {\n  /// The docker provider domain. Default: `docker.io`.\n  #[serde(default = \"default_docker_provider\")]\n  pub domain: String,\n  /// The accounts on the registry. Required.\n  #[serde(alias = \"account\")]\n  pub accounts: Vec<ProviderAccount>,\n  /// Available organizations on the registry provider.\n  /// Used to push an image under an organization's repo rather than an account's repo.\n  #[serde(default, alias = \"organization\")]\n  pub organizations: Vec<String>,\n}\n\nfn default_docker_provider() -> String {\n  String::from(\"docker.io\")\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Eq,\n  Hash,\n  PartialOrd,\n  Ord,\n  Serialize,\n  Deserialize,\n)]\npub struct ProviderAccount {\n  /// The account username. Required.\n  #[serde(alias = \"account\")]\n  pub username: String,\n  /// The account access token. Required.\n  #[serde(default, skip_serializing)]\n  pub token: String,\n}\n\npub fn empty_or_redacted(src: &str) -> String {\n  if src.is_empty() {\n    String::new()\n  } else {\n    String::from(\"##############\")\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/config/periphery.rs",
    "content": "//! # Configuring the Komodo Periphery Agent\n//!\n//! The periphery configuration is passed in three ways:\n//! 1. Command line args ([CliArgs])\n//! 2. Environment Variables ([Env])\n//! 3. Configuration File ([PeripheryConfig])\n//!\n//! The final configuration is built by combining parameters\n//! passed through the different methods. The priority of the args is\n//! strictly hierarchical, meaning params passed with [CliArgs] have top priority,\n//! followed by those passed in the environment, followed by those passed in\n//! the configuration file.\n//!\n\nuse clap::Parser;\nuse ipnetwork::IpNetwork;\nuse serde::Deserialize;\nuse std::{collections::HashMap, path::PathBuf};\n\nuse crate::{\n  deserializers::ForgivingVec,\n  entities::{\n    Timelength,\n    logger::{LogConfig, LogLevel, StdioLogMode},\n  },\n};\n\nuse super::{\n  DockerRegistry, GitProvider, ProviderAccount, empty_or_redacted,\n};\n\n/// # Periphery Command Line Arguments.\n///\n/// This structure represents the periphery command line arguments used to\n/// configure the periphery agent. A help manual for the periphery binary\n/// can be printed using `/path/to/periphery --help`.\n///\n/// Example command:\n/// ```sh\n/// periphery \\\n///   --config-path /path/to/periphery.config.base.toml \\\n///   --config-path /other_path/to/overide-periphery-config-directory \\\n///   --config-keyword periphery \\\n///   --config-keyword config \\\n///   --merge-nested-config true \\\n///   --extend-config-arrays false \\\n///   --log-level info\n/// ```\n#[derive(Parser)]\n#[command(name = \"periphery\", author, about, version)]\npub struct CliArgs {\n  /// Sets the path of a config file or directory to use.\n  /// Can use multiple times\n  #[arg(long, short = 'c')]\n  pub config_path: Option<Vec<PathBuf>>,\n\n  /// Sets the keywords to match directory periphery config file names on.\n  /// Supports wildcard syntax.\n  /// Can use multiple times to match multiple patterns independently.\n  #[arg(long, short = 'm')]\n  pub config_keyword: Option<Vec<String>>,\n\n  /// Merges nested configs, eg. secrets, providers.\n  /// Will override the equivalent env configuration.\n  /// Default: true\n  #[arg(long)]\n  pub merge_nested_config: Option<bool>,\n\n  /// Extends config arrays, eg. allowed_ips, passkeys.\n  /// Will override the equivalent env configuration.\n  /// Default: true\n  #[arg(long)]\n  pub extend_config_arrays: Option<bool>,\n\n  /// Configure the logging level: error, warn, info, debug, trace.\n  /// Default: info\n  /// If passed, will override any other log_level set.\n  #[arg(long)]\n  pub log_level: Option<tracing::Level>,\n}\n\n/// # Periphery Environment Variables\n///\n/// The variables should be passed in the traditional `UPPER_SNAKE_CASE` format,\n/// although the lower case format can still be parsed. If equivalent paramater is passed\n/// in [CliArgs], the value passed to the environment will be ignored in favor of the cli arg.\n#[derive(Deserialize)]\npub struct Env {\n  /// Specify the config paths (files or folders) used to build up the\n  /// final [PeripheryConfig].\n  /// If not provided, will use Default config.\n  ///\n  /// Note. This is overridden if the equivalent arg is passed in [CliArgs].\n  #[serde(default, alias = \"periphery_config_path\")]\n  pub periphery_config_paths: Vec<PathBuf>,\n  /// If specifying folders, use this to narrow down which\n  /// files will be matched to parse into the final [PeripheryConfig].\n  /// Only files inside the folders which have names containing a keywords\n  /// provided to `config_keywords` will be included.\n  /// Keywords support wildcard matching syntax.\n  ///\n  /// Note. This is overridden if the equivalent arg is passed in [CliArgs].\n  #[serde(\n    default = \"super::default_config_keywords\",\n    alias = \"periphery_config_keyword\"\n  )]\n  pub periphery_config_keywords: Vec<String>,\n\n  /// Will merge nested config object (eg. secrets, providers) across multiple\n  /// config files. Default: `true`\n  ///\n  /// Note. This is overridden if the equivalent arg is passed in [CliArgs].\n  #[serde(default = \"super::default_merge_nested_config\")]\n  pub periphery_merge_nested_config: bool,\n\n  /// Will extend config arrays (eg. `allowed_ips`, `passkeys`) across multiple config files.\n  /// Default: `true`\n  ///\n  /// Note. This is overridden if the equivalent arg is passed in [CliArgs].\n  #[serde(default = \"super::default_extend_config_arrays\")]\n  pub periphery_extend_config_arrays: bool,\n\n  /// Override `port`\n  pub periphery_port: Option<u16>,\n  /// Override `bind_ip`\n  pub periphery_bind_ip: Option<String>,\n  /// Override `root_directory`\n  pub periphery_root_directory: Option<PathBuf>,\n  /// Override `repo_dir`\n  pub periphery_repo_dir: Option<PathBuf>,\n  /// Override `stack_dir`\n  pub periphery_stack_dir: Option<PathBuf>,\n  /// Override `build_dir`\n  pub periphery_build_dir: Option<PathBuf>,\n  /// Override `disable_terminals`\n  pub periphery_disable_terminals: Option<bool>,\n  /// Override `disable_container_exec`\n  pub periphery_disable_container_exec: Option<bool>,\n  /// Override `stats_polling_rate`\n  pub periphery_stats_polling_rate: Option<Timelength>,\n  /// Override `container_stats_polling_rate`\n  pub periphery_container_stats_polling_rate: Option<Timelength>,\n  /// Override `legacy_compose_cli`\n  pub periphery_legacy_compose_cli: Option<bool>,\n\n  // LOGGING\n  /// Override `logging.level`\n  pub periphery_logging_level: Option<LogLevel>,\n  /// Override `logging.stdio`\n  pub periphery_logging_stdio: Option<StdioLogMode>,\n  /// Override `logging.pretty`\n  pub periphery_logging_pretty: Option<bool>,\n  /// Override `logging.location`\n  pub periphery_logging_location: Option<bool>,\n  /// Override `logging.otlp_endpoint`\n  pub periphery_logging_otlp_endpoint: Option<String>,\n  /// Override `logging.opentelemetry_service_name`\n  pub periphery_logging_opentelemetry_service_name: Option<String>,\n  /// Override `pretty_startup_config`\n  pub periphery_pretty_startup_config: Option<bool>,\n\n  /// Override `allowed_ips`\n  pub periphery_allowed_ips: Option<ForgivingVec<IpNetwork>>,\n  /// Override `passkeys`\n  pub periphery_passkeys: Option<Vec<String>>,\n  /// Override `passkeys` from file\n  pub periphery_passkeys_file: Option<PathBuf>,\n  /// Override `include_disk_mounts`\n  pub periphery_include_disk_mounts: Option<ForgivingVec<PathBuf>>,\n  /// Override `exclude_disk_mounts`\n  pub periphery_exclude_disk_mounts: Option<ForgivingVec<PathBuf>>,\n\n  /// Override `ssl_enabled`\n  pub periphery_ssl_enabled: Option<bool>,\n  /// Override `ssl_key_file`\n  pub periphery_ssl_key_file: Option<PathBuf>,\n  /// Override `ssl_cert_file`\n  pub periphery_ssl_cert_file: Option<PathBuf>,\n}\n\n/// # Periphery Configuration File\n///\n/// Refer to the [example file](https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml) for a full example.\n#[derive(Debug, Clone, Deserialize)]\npub struct PeripheryConfig {\n  /// The port periphery will run on.\n  /// Default: `8120`\n  #[serde(default = \"default_periphery_port\")]\n  pub port: u16,\n\n  /// IP address the periphery server binds to.\n  /// Default: [::].\n  #[serde(default = \"default_periphery_bind_ip\")]\n  pub bind_ip: String,\n\n  /// The directory Komodo will use as the default root for the specific (repo, stack, build) directories.\n  ///\n  /// repo: ${root_directory}/repos\n  /// stack: ${root_directory}/stacks\n  /// build: ${root_directory}/builds\n  ///\n  /// Note. These can each be overridden with a specific directory\n  /// by specifying `repo_dir`, `stack_dir`, or `build_dir` explicitly\n  ///\n  /// Default: `/etc/komodo`\n  #[serde(default = \"default_root_directory\")]\n  pub root_directory: PathBuf,\n\n  /// The system directory where Komodo managed repos will be cloned.\n  /// If not provided, will default to `${root_directory}/repos`.\n  /// Default: empty\n  pub repo_dir: Option<PathBuf>,\n\n  /// The system directory where stacks will managed.\n  /// If not provided, will default to `${root_directory}/stacks`.\n  /// Default: empty\n  pub stack_dir: Option<PathBuf>,\n\n  /// The system directory where builds will managed.\n  /// If not provided, will default to `${root_directory}/builds`.\n  /// Default: empty\n  pub build_dir: Option<PathBuf>,\n\n  /// Whether to disable the create terminal\n  /// and disallow direct remote shell access.\n  /// Default: false\n  #[serde(default)]\n  pub disable_terminals: bool,\n\n  /// Whether to disable the container exec api\n  /// and disallow remote container shell access.\n  /// Default: false\n  #[serde(default)]\n  pub disable_container_exec: bool,\n\n  /// The rate at which the system stats will be polled to update the cache.\n  /// Options: https://docs.rs/komodo_client/latest/komodo_client/entities/enum.Timelength.html\n  /// Default: `5-sec`\n  #[serde(default = \"default_stats_polling_rate\")]\n  pub stats_polling_rate: Timelength,\n\n  /// The rate at which the container stats will be polled to update the cache.\n  /// Options: https://docs.rs/komodo_client/latest/komodo_client/entities/enum.Timelength.html\n  /// Default: `30-sec`\n  #[serde(default = \"default_container_stats_polling_rate\")]\n  pub container_stats_polling_rate: Timelength,\n\n  /// Whether stack actions should use `docker-compose ...`\n  /// instead of `docker compose ...`.\n  /// Default: false\n  #[serde(default)]\n  pub legacy_compose_cli: bool,\n\n  /// Logging configuration\n  #[serde(default)]\n  pub logging: LogConfig,\n\n  /// Pretty-log (multi-line) the startup config\n  /// for easier human readability.\n  #[serde(default)]\n  pub pretty_startup_config: bool,\n\n  /// Limits which IP addresses are allowed to call the api.\n  /// Default: none\n  ///\n  /// Note: this should be configured to increase security.\n  #[serde(default)]\n  pub allowed_ips: ForgivingVec<IpNetwork>,\n\n  /// Limits the accepted passkeys.\n  /// Default: none\n  ///\n  /// Note: this should be configured to increase security.\n  #[serde(default)]\n  pub passkeys: Vec<String>,\n\n  /// If non-empty, only includes specific mount paths in the disk report.\n  #[serde(default)]\n  pub include_disk_mounts: ForgivingVec<PathBuf>,\n\n  /// Exclude specific mount paths in the disk report.\n  #[serde(default)]\n  pub exclude_disk_mounts: ForgivingVec<PathBuf>,\n\n  /// Mapping on local periphery secrets. These can be interpolated into eg. Deployment environment variables.\n  /// Default: none\n  #[serde(default)]\n  pub secrets: HashMap<String, String>,\n\n  /// Configure git credentials used to clone private repos.\n  /// Supports any git provider.\n  #[serde(default, alias = \"git_provider\")]\n  pub git_providers: ForgivingVec<GitProvider>,\n\n  /// Configure docker credentials used to push / pull images.\n  /// Supports any docker image repository.\n  #[serde(default, alias = \"docker_registry\")]\n  pub docker_registries: ForgivingVec<DockerRegistry>,\n\n  /// Whether to enable ssl.\n  /// Default: true\n  #[serde(default = \"default_ssl_enabled\")]\n  pub ssl_enabled: bool,\n\n  /// Path to the ssl key.\n  /// Default: `${root_directory}/ssl/key.pem`.\n  pub ssl_key_file: Option<PathBuf>,\n\n  /// Path to the ssl cert.\n  /// Default: `${root_directory}/ssl/cert.pem`.\n  pub ssl_cert_file: Option<PathBuf>,\n}\n\nfn default_periphery_port() -> u16 {\n  8120\n}\n\nfn default_periphery_bind_ip() -> String {\n  \"[::]\".to_string()\n}\n\nfn default_root_directory() -> PathBuf {\n  \"/etc/komodo\".parse().unwrap()\n}\n\nfn default_stats_polling_rate() -> Timelength {\n  Timelength::FiveSeconds\n}\n\nfn default_container_stats_polling_rate() -> Timelength {\n  Timelength::ThirtySeconds\n}\n\nfn default_ssl_enabled() -> bool {\n  true\n}\n\nimpl Default for PeripheryConfig {\n  fn default() -> Self {\n    Self {\n      port: default_periphery_port(),\n      bind_ip: default_periphery_bind_ip(),\n      root_directory: default_root_directory(),\n      repo_dir: None,\n      stack_dir: None,\n      build_dir: None,\n      disable_terminals: Default::default(),\n      disable_container_exec: Default::default(),\n      stats_polling_rate: default_stats_polling_rate(),\n      container_stats_polling_rate:\n        default_container_stats_polling_rate(),\n      legacy_compose_cli: Default::default(),\n      logging: Default::default(),\n      pretty_startup_config: Default::default(),\n      allowed_ips: Default::default(),\n      passkeys: Default::default(),\n      include_disk_mounts: Default::default(),\n      exclude_disk_mounts: Default::default(),\n      secrets: Default::default(),\n      git_providers: Default::default(),\n      docker_registries: Default::default(),\n      ssl_enabled: default_ssl_enabled(),\n      ssl_key_file: None,\n      ssl_cert_file: None,\n    }\n  }\n}\n\nimpl PeripheryConfig {\n  pub fn sanitized(&self) -> PeripheryConfig {\n    PeripheryConfig {\n      port: self.port,\n      bind_ip: self.bind_ip.clone(),\n      root_directory: self.root_directory.clone(),\n      repo_dir: self.repo_dir.clone(),\n      stack_dir: self.stack_dir.clone(),\n      build_dir: self.build_dir.clone(),\n      disable_terminals: self.disable_terminals,\n      disable_container_exec: self.disable_container_exec,\n      stats_polling_rate: self.stats_polling_rate,\n      container_stats_polling_rate: self.container_stats_polling_rate,\n      legacy_compose_cli: self.legacy_compose_cli,\n      logging: self.logging.clone(),\n      pretty_startup_config: self.pretty_startup_config,\n      allowed_ips: self.allowed_ips.clone(),\n      passkeys: self\n        .passkeys\n        .iter()\n        .map(|passkey| empty_or_redacted(passkey))\n        .collect(),\n      include_disk_mounts: self.include_disk_mounts.clone(),\n      exclude_disk_mounts: self.exclude_disk_mounts.clone(),\n      secrets: self\n        .secrets\n        .iter()\n        .map(|(var, secret)| {\n          (var.to_string(), empty_or_redacted(secret))\n        })\n        .collect(),\n      git_providers: self\n        .git_providers\n        .iter()\n        .map(|provider| GitProvider {\n          domain: provider.domain.clone(),\n          https: provider.https,\n          accounts: provider\n            .accounts\n            .iter()\n            .map(|account| ProviderAccount {\n              username: account.username.clone(),\n              token: empty_or_redacted(&account.token),\n            })\n            .collect(),\n        })\n        .collect(),\n      docker_registries: self\n        .docker_registries\n        .iter()\n        .map(|provider| DockerRegistry {\n          domain: provider.domain.clone(),\n          organizations: provider.organizations.clone(),\n          accounts: provider\n            .accounts\n            .iter()\n            .map(|account| ProviderAccount {\n              username: account.username.clone(),\n              token: empty_or_redacted(&account.token),\n            })\n            .collect(),\n        })\n        .collect(),\n      ssl_enabled: self.ssl_enabled,\n      ssl_key_file: self.ssl_key_file.clone(),\n      ssl_cert_file: self.ssl_cert_file.clone(),\n    }\n  }\n\n  pub fn repo_dir(&self) -> PathBuf {\n    if let Some(dir) = &self.repo_dir {\n      dir.to_owned()\n    } else {\n      self.root_directory.join(\"repos\")\n    }\n  }\n\n  pub fn stack_dir(&self) -> PathBuf {\n    if let Some(dir) = &self.stack_dir {\n      dir.to_owned()\n    } else {\n      self.root_directory.join(\"stacks\")\n    }\n  }\n\n  pub fn build_dir(&self) -> PathBuf {\n    if let Some(dir) = &self.build_dir {\n      dir.to_owned()\n    } else {\n      self.root_directory.join(\"builds\")\n    }\n  }\n\n  pub fn ssl_key_file(&self) -> PathBuf {\n    if let Some(dir) = &self.ssl_key_file {\n      dir.to_owned()\n    } else {\n      self.root_directory.join(\"ssl/key.pem\")\n    }\n  }\n\n  pub fn ssl_cert_file(&self) -> PathBuf {\n    if let Some(dir) = &self.ssl_cert_file {\n      dir.to_owned()\n    } else {\n      self.root_directory.join(\"ssl/cert.pem\")\n    }\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/deployment.rs",
    "content": "use anyhow::Context;\nuse bson::{Document, doc};\nuse derive_builder::Builder;\nuse derive_default_builder::DefaultBuilder;\nuse derive_variants::EnumVariants;\nuse partial_derive2::Partial;\nuse serde::{Deserialize, Serialize};\nuse strum::{Display, EnumString};\nuse typeshare::typeshare;\n\nuse crate::{\n  deserializers::{\n    conversions_deserializer, env_vars_deserializer,\n    labels_deserializer, option_conversions_deserializer,\n    option_env_vars_deserializer, option_labels_deserializer,\n    option_string_list_deserializer, option_term_labels_deserializer,\n    string_list_deserializer, term_labels_deserializer,\n  },\n  entities::{EnvironmentVar, environment_vars_from_str},\n  parsers::parse_key_value_list,\n};\n\nuse super::{\n  TerminationSignal, Version,\n  docker::container::ContainerStateStatusEnum,\n  resource::{Resource, ResourceListItem, ResourceQuery},\n};\n\n#[typeshare]\npub type Deployment = Resource<DeploymentConfig, ()>;\n\n#[typeshare]\npub type DeploymentListItem =\n  ResourceListItem<DeploymentListItemInfo>;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct DeploymentListItemInfo {\n  /// The state of the deployment / underlying docker container.\n  pub state: DeploymentState,\n  /// The status of the docker container (eg. up 12 hours, exited 5 minutes ago.)\n  pub status: Option<String>,\n  /// The image attached to the deployment.\n  pub image: String,\n  /// Whether there is a newer image available at the same tag.\n  pub update_available: bool,\n  /// The server that deployment sits on.\n  pub server_id: String,\n  /// An attached Komodo Build, if it exists.\n  pub build_id: Option<String>,\n}\n\n#[typeshare(serialized_as = \"Partial<DeploymentConfig>\")]\npub type _PartialDeploymentConfig = PartialDeploymentConfig;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[partial(skip_serializing_none, from, diff)]\npub struct DeploymentConfig {\n  /// The id of server the deployment is deployed on.\n  #[serde(default, alias = \"server\")]\n  #[partial_attr(serde(alias = \"server\"))]\n  #[builder(default)]\n  pub server_id: String,\n\n  /// The image which the deployment deploys.\n  /// Can either be a user inputted image, or a Komodo Build.\n  #[serde(default)]\n  #[builder(default)]\n  pub image: DeploymentImage,\n\n  /// Configure the account used to pull the image from the registry.\n  /// Used with `docker login`.\n  ///\n  ///  - If the field is empty string, will use the same account config as the build, or none at all if using image.\n  ///  - If the field contains an account, a token for the account must be available.\n  ///  - Will get the registry domain from the build / image\n  #[serde(default)]\n  #[builder(default)]\n  pub image_registry_account: String,\n\n  /// Whether to skip secret interpolation into the deployment environment variables.\n  #[serde(default)]\n  #[builder(default)]\n  pub skip_secret_interp: bool,\n\n  /// Whether to redeploy the deployment whenever the attached build finishes.\n  #[serde(default)]\n  #[builder(default)]\n  pub redeploy_on_build: bool,\n\n  /// Whether to poll for any updates to the image.\n  #[serde(default)]\n  #[builder(default)]\n  pub poll_for_updates: bool,\n\n  /// Whether to automatically redeploy when\n  /// newer a image is found. Will implicitly\n  /// enable `poll_for_updates`, you don't need to\n  /// enable both.\n  #[serde(default)]\n  #[builder(default)]\n  pub auto_update: bool,\n\n  /// Whether to send ContainerStateChange alerts for this deployment.\n  #[serde(default = \"default_send_alerts\")]\n  #[builder(default = \"default_send_alerts()\")]\n  #[partial_default(default_send_alerts())]\n  pub send_alerts: bool,\n\n  /// Configure quick links that are displayed in the resource header\n  #[serde(default)]\n  #[builder(default)]\n  pub links: Vec<String>,\n\n  /// The network attached to the container.\n  /// Default is `host`.\n  #[serde(default = \"default_network\")]\n  #[builder(default = \"default_network()\")]\n  #[partial_default(default_network())]\n  pub network: String,\n\n  /// The restart mode given to the container.\n  #[serde(default)]\n  #[builder(default)]\n  pub restart: RestartMode,\n\n  /// This is interpolated at the end of the `docker run` command,\n  /// which means they are either passed to the containers inner process,\n  /// or replaces the container command, depending on use of ENTRYPOINT or CMD in dockerfile.\n  /// Empty is no command.\n  #[serde(default)]\n  #[builder(default)]\n  pub command: String,\n\n  /// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).\n  #[serde(default)]\n  #[builder(default)]\n  pub termination_signal: TerminationSignal,\n\n  /// The termination timeout.\n  #[serde(default = \"default_termination_timeout\")]\n  #[builder(default = \"default_termination_timeout()\")]\n  #[partial_default(default_termination_timeout())]\n  pub termination_timeout: i32,\n\n  /// Extra args which are interpolated into the `docker run` command,\n  /// and affect the container configuration.\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub extra_args: Vec<String>,\n\n  /// Labels attached to various termination signal options.\n  /// Used to specify different shutdown functionality depending on the termination signal.\n  #[serde(default, deserialize_with = \"term_labels_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_term_labels_deserializer\"\n  ))]\n  #[builder(default)]\n  pub term_signal_labels: String,\n\n  /// The container port mapping.\n  /// Irrelevant if container network is `host`.\n  /// Maps ports on host to ports on container.\n  #[serde(default, deserialize_with = \"conversions_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_conversions_deserializer\"\n  ))]\n  #[builder(default)]\n  pub ports: String,\n\n  /// The container volume mapping.\n  /// Maps files / folders on host to files / folders in container.\n  #[serde(default, deserialize_with = \"conversions_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_conversions_deserializer\"\n  ))]\n  #[builder(default)]\n  pub volumes: String,\n\n  /// The environment variables passed to the container.\n  #[serde(default, deserialize_with = \"env_vars_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_env_vars_deserializer\"\n  ))]\n  #[builder(default)]\n  pub environment: String,\n\n  /// The docker labels given to the container.\n  #[serde(default, deserialize_with = \"labels_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_labels_deserializer\"\n  ))]\n  #[builder(default)]\n  pub labels: String,\n}\n\nimpl DeploymentConfig {\n  pub fn builder() -> DeploymentConfigBuilder {\n    DeploymentConfigBuilder::default()\n  }\n\n  pub fn env_vars(&self) -> anyhow::Result<Vec<EnvironmentVar>> {\n    environment_vars_from_str(&self.environment)\n      .context(\"Invalid environment\")\n  }\n}\n\nfn default_send_alerts() -> bool {\n  true\n}\n\nfn default_termination_timeout() -> i32 {\n  10\n}\n\nfn default_network() -> String {\n  String::from(\"host\")\n}\n\nimpl Default for DeploymentConfig {\n  fn default() -> Self {\n    Self {\n      server_id: Default::default(),\n      send_alerts: default_send_alerts(),\n      links: Default::default(),\n      image: Default::default(),\n      image_registry_account: Default::default(),\n      skip_secret_interp: Default::default(),\n      redeploy_on_build: Default::default(),\n      poll_for_updates: Default::default(),\n      auto_update: Default::default(),\n      term_signal_labels: Default::default(),\n      termination_signal: Default::default(),\n      termination_timeout: default_termination_timeout(),\n      ports: Default::default(),\n      volumes: Default::default(),\n      environment: Default::default(),\n      labels: Default::default(),\n      network: default_network(),\n      restart: Default::default(),\n      command: Default::default(),\n      extra_args: Default::default(),\n    }\n  }\n}\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, PartialEq, EnumVariants,\n)]\n#[variant_derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  Display,\n  EnumString\n)]\n#[serde(tag = \"type\", content = \"params\")]\npub enum DeploymentImage {\n  /// Deploy any external image.\n  Image {\n    /// The docker image, can be from any registry that works with docker and that the host server can reach.\n    #[serde(default)]\n    image: String,\n  },\n\n  /// Deploy a Komodo Build.\n  Build {\n    /// The id of the Build\n    #[serde(default, alias = \"build\")]\n    build_id: String,\n    /// Use a custom / older version of the image produced by the build.\n    /// if version is 0.0.0, this means `latest` image.\n    #[serde(default)]\n    version: Version,\n  },\n}\n\nimpl Default for DeploymentImage {\n  fn default() -> Self {\n    Self::Image {\n      image: Default::default(),\n    }\n  }\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct Conversion {\n  /// reference on the server.\n  pub local: String,\n  /// reference in the container.\n  pub container: String,\n}\n\npub fn conversions_from_str(\n  input: &str,\n) -> anyhow::Result<Vec<Conversion>> {\n  parse_key_value_list(input).map(|conversions| {\n    conversions\n      .into_iter()\n      .map(|(local, container)| Conversion { local, container })\n      .collect()\n  })\n}\n\n/// Variants de/serialized from/to snake_case.\n///\n/// Eg.\n/// - NotDeployed -> not_deployed\n/// - Restarting -> restarting\n/// - Running -> running.\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  Hash,\n  PartialOrd,\n  Ord,\n  Default,\n  Display,\n  EnumString,\n  Serialize,\n  Deserialize,\n)]\n#[serde(rename_all = \"snake_case\")]\n#[strum(serialize_all = \"snake_case\")]\npub enum DeploymentState {\n  /// The deployment is currently re/deploying\n  Deploying,\n  /// Container is running\n  Running,\n  /// Container is created but not running\n  Created,\n  /// Container is in restart loop\n  Restarting,\n  /// Container is being removed\n  Removing,\n  /// Container is paused\n  Paused,\n  /// Container is exited\n  Exited,\n  /// Container is dead\n  Dead,\n  /// The deployment is not deployed (no matching container)\n  NotDeployed,\n  /// Server not reachable for status\n  #[default]\n  Unknown,\n}\n\nimpl From<ContainerStateStatusEnum> for DeploymentState {\n  fn from(value: ContainerStateStatusEnum) -> Self {\n    match value {\n      ContainerStateStatusEnum::Empty => DeploymentState::Unknown,\n      ContainerStateStatusEnum::Created => DeploymentState::Created,\n      ContainerStateStatusEnum::Running => DeploymentState::Running,\n      ContainerStateStatusEnum::Paused => DeploymentState::Paused,\n      ContainerStateStatusEnum::Restarting => {\n        DeploymentState::Restarting\n      }\n      ContainerStateStatusEnum::Removing => DeploymentState::Removing,\n      ContainerStateStatusEnum::Exited => DeploymentState::Exited,\n      ContainerStateStatusEnum::Dead => DeploymentState::Dead,\n    }\n  }\n}\n\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  PartialEq,\n  Hash,\n  Eq,\n  Clone,\n  Copy,\n  Default,\n  Display,\n  EnumString,\n)]\npub enum RestartMode {\n  #[default]\n  #[serde(rename = \"no\")]\n  #[strum(serialize = \"no\")]\n  NoRestart,\n  #[serde(rename = \"on-failure\")]\n  #[strum(serialize = \"on-failure\")]\n  OnFailure,\n  #[serde(rename = \"always\")]\n  #[strum(serialize = \"always\")]\n  Always,\n  #[serde(rename = \"unless-stopped\")]\n  #[strum(serialize = \"unless-stopped\")]\n  UnlessStopped,\n}\n\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Clone,\n  Default,\n  PartialEq,\n  Eq,\n  Builder,\n)]\npub struct TerminationSignalLabel {\n  #[builder(default)]\n  pub signal: TerminationSignal,\n  #[builder(default)]\n  pub label: String,\n}\n\npub fn term_signal_labels_from_str(\n  input: &str,\n) -> anyhow::Result<Vec<TerminationSignalLabel>> {\n  parse_key_value_list(input).and_then(|list| {\n    list\n      .into_iter()\n      .map(|(signal, label)| {\n        anyhow::Ok(TerminationSignalLabel {\n          signal: signal.parse()?,\n          label,\n        })\n      })\n      .collect()\n  })\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]\npub struct DeploymentActionState {\n  pub pulling: bool,\n  pub deploying: bool,\n  pub starting: bool,\n  pub restarting: bool,\n  pub pausing: bool,\n  pub unpausing: bool,\n  pub stopping: bool,\n  pub destroying: bool,\n  pub renaming: bool,\n}\n\n#[typeshare]\npub type DeploymentQuery = ResourceQuery<DeploymentQuerySpecifics>;\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder,\n)]\npub struct DeploymentQuerySpecifics {\n  /// Query only for Deployments on these Servers.\n  /// If empty, does not filter by Server.\n  /// Only accepts Server id (not name).\n  #[serde(default)]\n  pub server_ids: Vec<String>,\n\n  /// Query only for Deployments with these Builds attached.\n  /// If empty, does not filter by Build.\n  /// Only accepts Build id (not name).\n  #[serde(default)]\n  pub build_ids: Vec<String>,\n\n  /// Query only for Deployments with available image updates.\n  #[serde(default)]\n  pub update_available: bool,\n}\n\nimpl super::resource::AddFilters for DeploymentQuerySpecifics {\n  fn add_filters(&self, filters: &mut Document) {\n    if !self.server_ids.is_empty() {\n      filters\n        .insert(\"config.server_id\", doc! { \"$in\": &self.server_ids });\n    }\n    if !self.build_ids.is_empty() {\n      filters.insert(\"config.image.type\", \"Build\");\n      filters.insert(\n        \"config.image.params.build_id\",\n        doc! { \"$in\": &self.build_ids },\n      );\n    }\n  }\n}\n\npub fn extract_registry_domain(\n  image_name: &str,\n) -> anyhow::Result<String> {\n  let mut split = image_name.split('/');\n  let maybe_domain =\n    split.next().context(\"image name cannot be empty string\")?;\n  if maybe_domain.contains('.') {\n    Ok(maybe_domain.to_string())\n  } else {\n    Ok(String::from(\"docker.io\"))\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/docker/container.rs",
    "content": "use std::collections::HashMap;\n\nuse anyhow::anyhow;\nuse serde::{Deserialize, Serialize};\nuse strum::Display;\nuse typeshare::typeshare;\n\nuse crate::entities::{I64, Usize};\n\nuse super::{ContainerConfig, GraphDriverData, PortBinding};\n\n/// Container summary returned by container list apis.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerListItem {\n  /// The Server which holds the container.\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub server_id: Option<String>,\n  /// The first name in Names, not including the initial '/'\n  pub name: String,\n  /// The ID of this container\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub id: Option<String>,\n  /// The name of the image used when creating this container\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub image: Option<String>,\n  /// The ID of the image that this container was created from\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub image_id: Option<String>,\n  /// When the container was created\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub created: Option<I64>,\n  /// The size of files that have been created or changed by this container\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub size_rw: Option<I64>,\n  /// The total size of all the files in this container\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub size_root_fs: Option<I64>,\n  /// The state of this container (e.g. `exited`)\n  pub state: ContainerStateStatusEnum,\n  /// Additional human-readable status of this container (e.g. `Exit 0`)\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub status: Option<String>,\n  /// The network mode\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub network_mode: Option<String>,\n  /// The network names attached to container\n  #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n  pub networks: Vec<String>,\n  /// Port mappings for the container\n  #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n  pub ports: Vec<Port>,\n  /// The volume names attached to container\n  #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n  pub volumes: Vec<String>,\n  /// The container stats, if they can be retreived.\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub stats: Option<ContainerStats>,\n  /// The labels attached to container.\n  /// It's too big to send with container list,\n  /// can get it using InspectContainer\n  #[serde(default, skip_serializing)]\n  pub labels: HashMap<String, String>,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct NameAndId {\n  pub name: String,\n  pub id: String,\n}\n\n/// An open port on a container\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct Port {\n  /// Host IP address that the container's port is mapped to\n  #[serde(rename = \"IP\")]\n  pub ip: Option<String>,\n\n  /// Port on the container\n  #[serde(default, rename = \"PrivatePort\")]\n  pub private_port: u16,\n\n  /// Port exposed on the host\n  #[serde(rename = \"PublicPort\")]\n  pub public_port: Option<u16>,\n\n  #[serde(default, rename = \"Type\")]\n  pub typ: PortTypeEnum,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n  Default,\n  Serialize,\n  Deserialize,\n)]\npub enum PortTypeEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  EMPTY,\n  #[serde(rename = \"tcp\")]\n  TCP,\n  #[serde(rename = \"udp\")]\n  UDP,\n  #[serde(rename = \"sctp\")]\n  SCTP,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct Container {\n  /// The ID of the container\n  #[serde(rename = \"Id\")]\n  pub id: Option<String>,\n\n  /// The time the container was created\n  #[serde(rename = \"Created\")]\n  pub created: Option<String>,\n\n  /// The path to the command being run\n  #[serde(rename = \"Path\")]\n  pub path: Option<String>,\n\n  /// The arguments to the command being run\n  #[serde(default, rename = \"Args\")]\n  pub args: Vec<String>,\n\n  #[serde(rename = \"State\")]\n  pub state: Option<ContainerState>,\n\n  /// The container's image ID\n  #[serde(rename = \"Image\")]\n  pub image: Option<String>,\n\n  #[serde(rename = \"ResolvConfPath\")]\n  pub resolv_conf_path: Option<String>,\n\n  #[serde(rename = \"HostnamePath\")]\n  pub hostname_path: Option<String>,\n\n  #[serde(rename = \"HostsPath\")]\n  pub hosts_path: Option<String>,\n\n  #[serde(rename = \"LogPath\")]\n  pub log_path: Option<String>,\n\n  #[serde(rename = \"Name\")]\n  pub name: Option<String>,\n\n  #[serde(rename = \"RestartCount\")]\n  pub restart_count: Option<I64>,\n\n  #[serde(rename = \"Driver\")]\n  pub driver: Option<String>,\n\n  #[serde(rename = \"Platform\")]\n  pub platform: Option<String>,\n\n  #[serde(rename = \"MountLabel\")]\n  pub mount_label: Option<String>,\n\n  #[serde(rename = \"ProcessLabel\")]\n  pub process_label: Option<String>,\n\n  #[serde(rename = \"AppArmorProfile\")]\n  pub app_armor_profile: Option<String>,\n\n  /// IDs of exec instances that are running in the container.\n  #[serde(default, rename = \"ExecIDs\")]\n  pub exec_ids: Vec<String>,\n\n  #[serde(rename = \"HostConfig\")]\n  pub host_config: Option<HostConfig>,\n\n  #[serde(rename = \"GraphDriver\")]\n  pub graph_driver: Option<GraphDriverData>,\n\n  /// The size of files that have been created or changed by this container.\n  #[serde(rename = \"SizeRw\")]\n  pub size_rw: Option<I64>,\n\n  /// The total size of all the files in this container.\n  #[serde(rename = \"SizeRootFs\")]\n  pub size_root_fs: Option<I64>,\n\n  #[serde(default, rename = \"Mounts\")]\n  pub mounts: Vec<MountPoint>,\n\n  #[serde(rename = \"Config\")]\n  pub config: Option<ContainerConfig>,\n\n  #[serde(rename = \"NetworkSettings\")]\n  pub network_settings: Option<NetworkSettings>,\n}\n\n/// ContainerState stores container's running state. It's part of ContainerJSONBase and will be returned by the \\\"inspect\\\" command.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerState {\n  /// String representation of the container state. Can be one of \\\"created\\\", \\\"running\\\", \\\"paused\\\", \\\"restarting\\\", \\\"removing\\\", \\\"exited\\\", or \\\"dead\\\".\n  #[serde(default, rename = \"Status\")]\n  pub status: ContainerStateStatusEnum,\n\n  /// Whether this container is running.  Note that a running container can be _paused_. The `Running` and `Paused` booleans are not mutually exclusive:  When pausing a container (on Linux), the freezer cgroup is used to suspend all processes in the container. Freezing the process requires the process to be running. As a result, paused containers are both `Running` _and_ `Paused`.  Use the `Status` field instead to determine if a container's state is \\\"running\\\".\n  #[serde(rename = \"Running\")]\n  pub running: Option<bool>,\n\n  /// Whether this container is paused.\n  #[serde(rename = \"Paused\")]\n  pub paused: Option<bool>,\n\n  /// Whether this container is restarting.\n  #[serde(rename = \"Restarting\")]\n  pub restarting: Option<bool>,\n\n  /// Whether a process within this container has been killed because it ran out of memory since the container was last started.\n  #[serde(rename = \"OOMKilled\")]\n  pub oom_killed: Option<bool>,\n\n  #[serde(rename = \"Dead\")]\n  pub dead: Option<bool>,\n\n  /// The process ID of this container\n  #[serde(rename = \"Pid\")]\n  pub pid: Option<I64>,\n\n  /// The last exit code of this container\n  #[serde(rename = \"ExitCode\")]\n  pub exit_code: Option<I64>,\n\n  #[serde(rename = \"Error\")]\n  pub error: Option<String>,\n\n  /// The time when this container was last started.\n  #[serde(rename = \"StartedAt\")]\n  pub started_at: Option<String>,\n\n  /// The time when this container last exited.\n  #[serde(rename = \"FinishedAt\")]\n  pub finished_at: Option<String>,\n\n  #[serde(rename = \"Health\")]\n  pub health: Option<ContainerHealth>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  Default,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n  Display,\n  Serialize,\n  Deserialize,\n)]\n#[serde(rename_all = \"lowercase\")]\n#[strum(serialize_all = \"lowercase\")]\npub enum ContainerStateStatusEnum {\n  Running,\n  Created,\n  Paused,\n  Restarting,\n  Exited,\n  Removing,\n  Dead,\n  #[default]\n  #[serde(rename = \"\")]\n  #[strum(serialize = \"\")]\n  Empty,\n}\n\nimpl ::std::str::FromStr for ContainerStateStatusEnum {\n  type Err = anyhow::Error;\n  fn from_str(s: &str) -> Result<Self, Self::Err> {\n    match s {\n      \"\" => Ok(ContainerStateStatusEnum::Empty),\n      \"created\" => Ok(ContainerStateStatusEnum::Created),\n      \"running\" => Ok(ContainerStateStatusEnum::Running),\n      \"paused\" => Ok(ContainerStateStatusEnum::Paused),\n      \"restarting\" => Ok(ContainerStateStatusEnum::Restarting),\n      \"removing\" => Ok(ContainerStateStatusEnum::Removing),\n      \"exited\" => Ok(ContainerStateStatusEnum::Exited),\n      \"dead\" => Ok(ContainerStateStatusEnum::Dead),\n      x => Err(anyhow!(\"Invalid container state: {}\", x)),\n    }\n  }\n}\n\n/// Health stores information about the container's healthcheck results.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerHealth {\n  /// Status is one of `none`, `starting`, `healthy` or `unhealthy`  - \\\"none\\\"      Indicates there is no healthcheck - \\\"starting\\\"  Starting indicates that the container is not yet ready - \\\"healthy\\\"   Healthy indicates that the container is running correctly - \\\"unhealthy\\\" Unhealthy indicates that the container has a problem\n  #[serde(default, rename = \"Status\")]\n  pub status: HealthStatusEnum,\n\n  /// FailingStreak is the number of consecutive failures\n  #[serde(rename = \"FailingStreak\")]\n  pub failing_streak: Option<I64>,\n\n  /// Log contains the last few results (oldest first)\n  #[serde(default, rename = \"Log\")]\n  pub log: Vec<HealthcheckResult>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum HealthStatusEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"none\")]\n  None,\n  #[serde(rename = \"starting\")]\n  Starting,\n  #[serde(rename = \"healthy\")]\n  Healthy,\n  #[serde(rename = \"unhealthy\")]\n  Unhealthy,\n}\n\n/// HealthcheckResult stores information about a single run of a healthcheck probe\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct HealthcheckResult {\n  /// Date and time at which this check started in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.\n  #[serde(rename = \"Start\")]\n  pub start: Option<String>,\n\n  /// Date and time at which this check ended in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.\n  #[serde(rename = \"End\")]\n  pub end: Option<String>,\n\n  /// ExitCode meanings:  - `0` healthy - `1` unhealthy - `2` reserved (considered unhealthy) - other values: error running probe\n  #[serde(rename = \"ExitCode\")]\n  pub exit_code: Option<I64>,\n\n  /// Output from last check\n  #[serde(rename = \"Output\")]\n  pub output: Option<String>,\n}\n\n/// Container configuration that depends on the host we are running on\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct HostConfig {\n  /// An integer value representing this container's relative CPU weight versus other containers.\n  #[serde(rename = \"CpuShares\")]\n  pub cpu_shares: Option<I64>,\n\n  /// Memory limit in bytes.\n  #[serde(rename = \"Memory\")]\n  pub memory: Option<I64>,\n\n  /// Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist.\n  #[serde(rename = \"CgroupParent\")]\n  pub cgroup_parent: Option<String>,\n\n  /// Block IO weight (relative weight).\n  #[serde(rename = \"BlkioWeight\")]\n  pub blkio_weight: Option<u16>,\n\n  /// Block IO weight (relative device weight) in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Weight\\\": weight}] ```\n  #[serde(default, rename = \"BlkioWeightDevice\")]\n  pub blkio_weight_device: Vec<ResourcesBlkioWeightDevice>,\n\n  /// Limit read rate (bytes per second) from a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ```\n  #[serde(default, rename = \"BlkioDeviceReadBps\")]\n  pub blkio_device_read_bps: Vec<ThrottleDevice>,\n\n  /// Limit write rate (bytes per second) to a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ```\n  #[serde(default, rename = \"BlkioDeviceWriteBps\")]\n  pub blkio_device_write_bps: Vec<ThrottleDevice>,\n\n  /// Limit read rate (IO per second) from a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ```\n  #[serde(default, rename = \"BlkioDeviceReadIOps\")]\n  pub blkio_device_read_iops: Vec<ThrottleDevice>,\n\n  /// Limit write rate (IO per second) to a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ```\n  #[serde(default, rename = \"BlkioDeviceWriteIOps\")]\n  pub blkio_device_write_iops: Vec<ThrottleDevice>,\n\n  /// The length of a CPU period in microseconds.\n  #[serde(rename = \"CpuPeriod\")]\n  pub cpu_period: Option<I64>,\n\n  /// Microseconds of CPU time that the container can get in a CPU period.\n  #[serde(rename = \"CpuQuota\")]\n  pub cpu_quota: Option<I64>,\n\n  /// The length of a CPU real-time period in microseconds. Set to 0 to allocate no time allocated to real-time tasks.\n  #[serde(rename = \"CpuRealtimePeriod\")]\n  pub cpu_realtime_period: Option<I64>,\n\n  /// The length of a CPU real-time runtime in microseconds. Set to 0 to allocate no time allocated to real-time tasks.\n  #[serde(rename = \"CpuRealtimeRuntime\")]\n  pub cpu_realtime_runtime: Option<I64>,\n\n  /// CPUs in which to allow execution (e.g., `0-3`, `0,1`).\n  #[serde(rename = \"CpusetCpus\")]\n  pub cpuset_cpus: Option<String>,\n\n  /// Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems.\n  #[serde(rename = \"CpusetMems\")]\n  pub cpuset_mems: Option<String>,\n\n  /// A list of devices to add to the container.\n  #[serde(default, rename = \"Devices\")]\n  pub devices: Vec<DeviceMapping>,\n\n  /// a list of cgroup rules to apply to the container\n  #[serde(default, rename = \"DeviceCgroupRules\")]\n  pub device_cgroup_rules: Vec<String>,\n\n  /// A list of requests for devices to be sent to device drivers.\n  #[serde(default, rename = \"DeviceRequests\")]\n  pub device_requests: Vec<DeviceRequest>,\n\n  /// Hard limit for kernel TCP buffer memory (in bytes). Depending on the OCI runtime in use, this option may be ignored. It is no longer supported by the default (runc) runtime.  This field is omitted when empty.\n  #[serde(rename = \"KernelMemoryTCP\")]\n  pub kernel_memory_tcp: Option<I64>,\n\n  /// Memory soft limit in bytes.\n  #[serde(rename = \"MemoryReservation\")]\n  pub memory_reservation: Option<I64>,\n\n  /// Total memory limit (memory + swap). Set as `-1` to enable unlimited swap.\n  #[serde(rename = \"MemorySwap\")]\n  pub memory_swap: Option<I64>,\n\n  /// Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100.\n  #[serde(rename = \"MemorySwappiness\")]\n  pub memory_swappiness: Option<I64>,\n\n  /// CPU quota in units of 10<sup>-9</sup> CPUs.\n  #[serde(rename = \"NanoCpus\")]\n  pub nano_cpus: Option<I64>,\n\n  /// Disable OOM Killer for the container.\n  #[serde(rename = \"OomKillDisable\")]\n  pub oom_kill_disable: Option<bool>,\n\n  /// Run an init inside the container that forwards signals and reaps processes. This field is omitted if empty, and the default (as configured on the daemon) is used.\n  #[serde(rename = \"Init\")]\n  pub init: Option<bool>,\n\n  /// Tune a container's PIDs limit. Set `0` or `-1` for unlimited, or `null` to not change.\n  #[serde(rename = \"PidsLimit\")]\n  pub pids_limit: Option<I64>,\n\n  /// A list of resource limits to set in the container. For example:  ``` {\\\"Name\\\": \\\"nofile\\\", \\\"Soft\\\": 1024, \\\"Hard\\\": 2048} ```\n  #[serde(default, rename = \"Ulimits\")]\n  pub ulimits: Vec<ResourcesUlimits>,\n\n  /// The number of usable CPUs (Windows only).  On Windows Server containers, the processor resource controls are mutually exclusive. The order of precedence is `CPUCount` first, then `CPUShares`, and `CPUPercent` last.\n  #[serde(rename = \"CpuCount\")]\n  pub cpu_count: Option<I64>,\n\n  /// The usable percentage of the available CPUs (Windows only).  On Windows Server containers, the processor resource controls are mutually exclusive. The order of precedence is `CPUCount` first, then `CPUShares`, and `CPUPercent` last.\n  #[serde(rename = \"CpuPercent\")]\n  pub cpu_percent: Option<I64>,\n\n  /// Maximum IOps for the container system drive (Windows only)\n  #[serde(rename = \"IOMaximumIOps\")]\n  pub io_maximum_iops: Option<I64>,\n\n  /// Maximum IO in bytes per second for the container system drive (Windows only).\n  #[serde(rename = \"IOMaximumBandwidth\")]\n  pub io_maximum_bandwidth: Option<I64>,\n\n  /// A list of volume bindings for this container. Each volume binding is a string in one of these forms:  - `host-src:container-dest[:options]` to bind-mount a host path   into the container. Both `host-src`, and `container-dest` must   be an _absolute_ path. - `volume-name:container-dest[:options]` to bind-mount a volume   managed by a volume driver into the container. `container-dest`   must be an _absolute_ path.  `options` is an optional, comma-delimited list of:  - `nocopy` disables automatic copying of data from the container   path to the volume. The `nocopy` flag only applies to named volumes. - `[ro|rw]` mounts a volume read-only or read-write, respectively.   If omitted or set to `rw`, volumes are mounted read-write. - `[z|Z]` applies SELinux labels to allow or deny multiple containers   to read and write to the same volume.     - `z`: a _shared_ content label is applied to the content. This       label indicates that multiple containers can share the volume       content, for both reading and writing.     - `Z`: a _private unshared_ label is applied to the content.       This label indicates that only the current container can use       a private volume. Labeling systems such as SELinux require       proper labels to be placed on volume content that is mounted       into a container. Without a label, the security system can       prevent a container's processes from using the content. By       default, the labels set by the host operating system are not       modified. - `[[r]shared|[r]slave|[r]private]` specifies mount   [propagation behavior](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt).   This only applies to bind-mounted volumes, not internal volumes   or named volumes. Mount propagation requires the source mount   point (the location where the source directory is mounted in the   host operating system) to have the correct propagation properties.   For shared volumes, the source mount point must be set to `shared`.   For slave volumes, the mount must be set to either `shared` or   `slave`.\n  #[serde(default, rename = \"Binds\")]\n  pub binds: Vec<String>,\n\n  /// Path to a file where the container ID is written\n  #[serde(rename = \"ContainerIDFile\")]\n  pub container_id_file: Option<String>,\n\n  #[serde(rename = \"LogConfig\")]\n  pub log_config: Option<HostConfigLogConfig>,\n\n  /// Network mode to use for this container. Supported standard values are: `bridge`, `host`, `none`, and `container:<name|id>`. Any other value is taken as a custom network's name to which this container should connect to.\n  #[serde(rename = \"NetworkMode\")]\n  pub network_mode: Option<String>,\n\n  #[serde(default, rename = \"PortBindings\")]\n  pub port_bindings: HashMap<String, Vec<PortBinding>>,\n\n  #[serde(rename = \"RestartPolicy\")]\n  pub restart_policy: Option<RestartPolicy>,\n\n  /// Automatically remove the container when the container's process exits. This has no effect if `RestartPolicy` is set.\n  #[serde(rename = \"AutoRemove\")]\n  pub auto_remove: Option<bool>,\n\n  /// Driver that this container uses to mount volumes.\n  #[serde(rename = \"VolumeDriver\")]\n  pub volume_driver: Option<String>,\n\n  /// A list of volumes to inherit from another container, specified in the form `<container name>[:<ro|rw>]`.\n  #[serde(default, rename = \"VolumesFrom\")]\n  pub volumes_from: Vec<String>,\n\n  /// Specification for mounts to be added to the container.\n  #[serde(default, rename = \"Mounts\")]\n  pub mounts: Vec<ContainerMount>,\n\n  /// Initial console size, as an `[height, width]` array.\n  #[serde(default, rename = \"ConsoleSize\")]\n  pub console_size: Vec<i32>,\n\n  /// Arbitrary non-identifying metadata attached to container and provided to the runtime when the container is started.\n  #[serde(default, rename = \"Annotations\")]\n  pub annotations: HashMap<String, String>,\n\n  /// A list of kernel capabilities to add to the container. Conflicts with option 'Capabilities'.\n  #[serde(default, rename = \"CapAdd\")]\n  pub cap_add: Vec<String>,\n\n  /// A list of kernel capabilities to drop from the container. Conflicts with option 'Capabilities'.\n  #[serde(default, rename = \"CapDrop\")]\n  pub cap_drop: Vec<String>,\n\n  /// cgroup namespace mode for the container. Possible values are:  - `\\\"private\\\"`: the container runs in its own private cgroup namespace - `\\\"host\\\"`: use the host system's cgroup namespace  If not specified, the daemon default is used, which can either be `\\\"private\\\"` or `\\\"host\\\"`, depending on daemon version, kernel support and configuration.\n  #[serde(rename = \"CgroupnsMode\")]\n  pub cgroupns_mode: Option<HostConfigCgroupnsModeEnum>,\n\n  /// A list of DNS servers for the container to use.\n  #[serde(default, rename = \"Dns\")]\n  pub dns: Vec<String>,\n\n  /// A list of DNS options.\n  #[serde(default, rename = \"DnsOptions\")]\n  pub dns_options: Vec<String>,\n\n  /// A list of DNS search domains.\n  #[serde(default, rename = \"DnsSearch\")]\n  pub dns_search: Vec<String>,\n\n  /// A list of hostnames/IP mappings to add to the container's `/etc/hosts` file. Specified in the form `[\\\"hostname:IP\\\"]`.\n  #[serde(default, rename = \"ExtraHosts\")]\n  pub extra_hosts: Vec<String>,\n\n  /// A list of additional groups that the container process will run as.\n  #[serde(default, rename = \"GroupAdd\")]\n  pub group_add: Vec<String>,\n\n  /// IPC sharing mode for the container. Possible values are:  - `\\\"none\\\"`: own private IPC namespace, with /dev/shm not mounted - `\\\"private\\\"`: own private IPC namespace - `\\\"shareable\\\"`: own private IPC namespace, with a possibility to share it with other containers - `\\\"container:<name|id>\\\"`: join another (shareable) container's IPC namespace - `\\\"host\\\"`: use the host system's IPC namespace  If not specified, daemon default is used, which can either be `\\\"private\\\"` or `\\\"shareable\\\"`, depending on daemon version and configuration.\n  #[serde(rename = \"IpcMode\")]\n  pub ipc_mode: Option<String>,\n\n  /// Cgroup to use for the container.\n  #[serde(rename = \"Cgroup\")]\n  pub cgroup: Option<String>,\n\n  /// A list of links for the container in the form `container_name:alias`.\n  #[serde(default, rename = \"Links\")]\n  pub links: Vec<String>,\n\n  /// An integer value containing the score given to the container in order to tune OOM killer preferences.\n  #[serde(rename = \"OomScoreAdj\")]\n  pub oom_score_adj: Option<I64>,\n\n  /// Set the PID (Process) Namespace mode for the container. It can be either:  - `\\\"container:<name|id>\\\"`: joins another container's PID namespace - `\\\"host\\\"`: use the host's PID namespace inside the container\n  #[serde(rename = \"PidMode\")]\n  pub pid_mode: Option<String>,\n\n  /// Gives the container full access to the host.\n  #[serde(rename = \"Privileged\")]\n  pub privileged: Option<bool>,\n\n  /// Allocates an ephemeral host port for all of a container's exposed ports.  Ports are de-allocated when the container stops and allocated when the container starts. The allocated port might be changed when restarting the container.  The port is selected from the ephemeral port range that depends on the kernel. For example, on Linux the range is defined by `/proc/sys/net/ipv4/ip_local_port_range`.\n  #[serde(rename = \"PublishAllPorts\")]\n  pub publish_all_ports: Option<bool>,\n\n  /// Mount the container's root filesystem as read only.\n  #[serde(rename = \"ReadonlyRootfs\")]\n  pub readonly_rootfs: Option<bool>,\n\n  /// A list of string values to customize labels for MLS systems, such as SELinux.\n  #[serde(default, rename = \"SecurityOpt\")]\n  pub security_opt: Vec<String>,\n\n  /// Storage driver options for this container, in the form `{\\\"size\\\": \\\"120G\\\"}`.\n  #[serde(default, rename = \"StorageOpt\")]\n  pub storage_opt: HashMap<String, String>,\n\n  /// A map of container directories which should be replaced by tmpfs mounts, and their corresponding mount options. For example:  ``` { \\\"/run\\\": \\\"rw,noexec,nosuid,size=65536k\\\" } ```\n  #[serde(default, rename = \"Tmpfs\")]\n  pub tmpfs: HashMap<String, String>,\n\n  /// UTS namespace to use for the container.\n  #[serde(rename = \"UTSMode\")]\n  pub uts_mode: Option<String>,\n\n  /// Sets the usernamespace mode for the container when usernamespace remapping option is enabled.\n  #[serde(rename = \"UsernsMode\")]\n  pub userns_mode: Option<String>,\n\n  /// Size of `/dev/shm` in bytes. If omitted, the system uses 64MB.\n  #[serde(rename = \"ShmSize\")]\n  pub shm_size: Option<I64>,\n\n  /// A list of kernel parameters (sysctls) to set in the container. For example:  ``` {\\\"net.ipv4.ip_forward\\\": \\\"1\\\"} ```\n  #[serde(default, rename = \"Sysctls\")]\n  pub sysctls: HashMap<String, String>,\n\n  /// Runtime to use with this container.\n  #[serde(rename = \"Runtime\")]\n  pub runtime: Option<String>,\n\n  /// Isolation technology of the container. (Windows only)\n  #[serde(default, rename = \"Isolation\")]\n  pub isolation: HostConfigIsolationEnum,\n\n  /// The list of paths to be masked inside the container (this overrides the default set of paths).\n  #[serde(default, rename = \"MaskedPaths\")]\n  pub masked_paths: Vec<String>,\n\n  /// The list of paths to be set as read-only inside the container (this overrides the default set of paths).\n  #[serde(default, rename = \"ReadonlyPaths\")]\n  pub readonly_paths: Vec<String>,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ResourcesBlkioWeightDevice {\n  #[serde(rename = \"Path\")]\n  pub path: Option<String>,\n\n  #[serde(rename = \"Weight\")]\n  pub weight: Option<Usize>,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ThrottleDevice {\n  /// Device path\n  #[serde(rename = \"Path\")]\n  pub path: Option<String>,\n\n  /// Rate\n  #[serde(rename = \"Rate\")]\n  pub rate: Option<I64>,\n}\n\n/// A device mapping between the host and container\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct DeviceMapping {\n  #[serde(rename = \"PathOnHost\")]\n  pub path_on_host: Option<String>,\n\n  #[serde(rename = \"PathInContainer\")]\n  pub path_in_container: Option<String>,\n\n  #[serde(rename = \"CgroupPermissions\")]\n  pub cgroup_permissions: Option<String>,\n}\n\n/// A request for devices to be sent to device drivers\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct DeviceRequest {\n  #[serde(rename = \"Driver\")]\n  pub driver: Option<String>,\n\n  #[serde(rename = \"Count\")]\n  pub count: Option<I64>,\n\n  #[serde(default, rename = \"DeviceIDs\")]\n  pub device_ids: Vec<String>,\n\n  /// A list of capabilities; an OR list of AND lists of capabilities.\n  #[serde(default, rename = \"Capabilities\")]\n  pub capabilities: Vec<Vec<String>>,\n\n  /// Driver-specific options, specified as a key/value pairs. These options are passed directly to the driver.\n  #[serde(default, rename = \"Options\")]\n  pub options: HashMap<String, String>,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ResourcesUlimits {\n  /// Name of ulimit\n  #[serde(rename = \"Name\")]\n  pub name: Option<String>,\n\n  /// Soft limit\n  #[serde(rename = \"Soft\")]\n  pub soft: Option<I64>,\n\n  /// Hard limit\n  #[serde(rename = \"Hard\")]\n  pub hard: Option<I64>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum HostConfigIsolationEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"default\")]\n  Default,\n  #[serde(rename = \"process\")]\n  Process,\n  #[serde(rename = \"hyperv\")]\n  Hyperv,\n}\n\n/// The logging configuration for this container\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct HostConfigLogConfig {\n  #[serde(rename = \"Type\")]\n  pub typ: Option<String>,\n\n  #[serde(default, rename = \"Config\")]\n  pub config: HashMap<String, String>,\n}\n\n/// The behavior to apply when the container exits. The default is not to restart.  An ever increasing delay (double the previous delay, starting at 100ms) is added before each restart to prevent flooding the server.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct RestartPolicy {\n  /// - Empty string means not to restart - `no` Do not automatically restart - `always` Always restart - `unless-stopped` Restart always except when the user has manually stopped the container - `on-failure` Restart only when the container exit code is non-zero\n  #[serde(default, rename = \"Name\")]\n  pub name: RestartPolicyNameEnum,\n\n  /// If `on-failure` is used, the number of times to retry before giving up.\n  #[serde(rename = \"MaximumRetryCount\")]\n  pub maximum_retry_count: Option<I64>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum RestartPolicyNameEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"no\")]\n  No,\n  #[serde(rename = \"always\")]\n  Always,\n  #[serde(rename = \"unless-stopped\")]\n  UnlessStopped,\n  #[serde(rename = \"on-failure\")]\n  OnFailure,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerMount {\n  /// Container path.\n  #[serde(rename = \"Target\")]\n  pub target: Option<String>,\n\n  /// Mount source (e.g. a volume name, a host path).\n  #[serde(rename = \"Source\")]\n  pub source: Option<String>,\n\n  /// The mount type. Available types:  - `bind` Mounts a file or directory from the host into the container. Must exist prior to creating the container. - `volume` Creates a volume with the given name and options (or uses a pre-existing volume with the same name and options). These are **not** removed when the container is removed. - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container. - `cluster` a Swarm cluster volume\n  #[serde(default, rename = \"Type\")]\n  pub typ: MountTypeEnum,\n\n  /// Whether the mount should be read-only.\n  #[serde(rename = \"ReadOnly\")]\n  pub read_only: Option<bool>,\n\n  /// The consistency requirement for the mount: `default`, `consistent`, `cached`, or `delegated`.\n  #[serde(rename = \"Consistency\")]\n  pub consistency: Option<String>,\n\n  #[serde(rename = \"BindOptions\")]\n  pub bind_options: Option<MountBindOptions>,\n\n  #[serde(rename = \"VolumeOptions\")]\n  pub volume_options: Option<MountVolumeOptions>,\n\n  #[serde(rename = \"TmpfsOptions\")]\n  pub tmpfs_options: Option<MountTmpfsOptions>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum MountTypeEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"bind\")]\n  Bind,\n  #[serde(rename = \"volume\")]\n  Volume,\n  #[serde(rename = \"image\")]\n  Image,\n  #[serde(rename = \"tmpfs\")]\n  Tmpfs,\n  #[serde(rename = \"npipe\")]\n  Npipe,\n  #[serde(rename = \"cluster\")]\n  Cluster,\n}\n\n/// Optional configuration for the `bind` type.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct MountBindOptions {\n  /// A propagation mode with the value `[r]private`, `[r]shared`, or `[r]slave`.\n  #[serde(default, rename = \"Propagation\")]\n  pub propagation: MountBindOptionsPropagationEnum,\n\n  /// Disable recursive bind mount.\n  #[serde(rename = \"NonRecursive\")]\n  pub non_recursive: Option<bool>,\n\n  /// Create mount point on host if missing\n  #[serde(rename = \"CreateMountpoint\")]\n  pub create_mountpoint: Option<bool>,\n\n  /// Make the mount non-recursively read-only, but still leave the mount recursive (unless NonRecursive is set to `true` in conjunction).  Addded in v1.44, before that version all read-only mounts were non-recursive by default. To match the previous behaviour this will default to `true` for clients on versions prior to v1.44.\n  #[serde(rename = \"ReadOnlyNonRecursive\")]\n  pub read_only_non_recursive: Option<bool>,\n\n  /// Raise an error if the mount cannot be made recursively read-only.\n  #[serde(rename = \"ReadOnlyForceRecursive\")]\n  pub read_only_force_recursive: Option<bool>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum MountBindOptionsPropagationEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"private\")]\n  Private,\n  #[serde(rename = \"rprivate\")]\n  Rprivate,\n  #[serde(rename = \"shared\")]\n  Shared,\n  #[serde(rename = \"rshared\")]\n  Rshared,\n  #[serde(rename = \"slave\")]\n  Slave,\n  #[serde(rename = \"rslave\")]\n  Rslave,\n}\n\n/// Optional configuration for the `volume` type.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct MountVolumeOptions {\n  /// Populate volume with data from the target.\n  #[serde(rename = \"NoCopy\")]\n  pub no_copy: Option<bool>,\n\n  /// User-defined key/value metadata.\n  #[serde(default, rename = \"Labels\")]\n  pub labels: HashMap<String, String>,\n\n  #[serde(rename = \"DriverConfig\")]\n  pub driver_config: Option<MountVolumeOptionsDriverConfig>,\n\n  /// Source path inside the volume. Must be relative without any back traversals.\n  #[serde(rename = \"Subpath\")]\n  pub subpath: Option<String>,\n}\n\n/// Map of driver specific options\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct MountVolumeOptionsDriverConfig {\n  /// Name of the driver to use to create the volume.\n  #[serde(rename = \"Name\")]\n  pub name: Option<String>,\n\n  /// key/value map of driver specific options.\n  #[serde(default, rename = \"Options\")]\n  pub options: HashMap<String, String>,\n}\n\n/// Optional configuration for the `tmpfs` type.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct MountTmpfsOptions {\n  /// The size for the tmpfs mount in bytes.\n  #[serde(rename = \"SizeBytes\")]\n  pub size_bytes: Option<I64>,\n\n  /// The permission mode for the tmpfs mount in an integer.\n  #[serde(rename = \"Mode\")]\n  pub mode: Option<I64>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum HostConfigCgroupnsModeEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"private\")]\n  Private,\n  #[serde(rename = \"host\")]\n  Host,\n}\n\n/// MountPoint represents a mount point configuration inside the container. This is used for reporting the mountpoints in use by a container.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct MountPoint {\n  /// The mount type:  - `bind` a mount of a file or directory from the host into the container. - `volume` a docker volume with the given `Name`. - `tmpfs` a `tmpfs`. - `npipe` a named pipe from the host into the container. - `cluster` a Swarm cluster volume\n  #[serde(default, rename = \"Type\")]\n  pub typ: MountTypeEnum,\n\n  /// Name is the name reference to the underlying data defined by `Source` e.g., the volume name.\n  #[serde(rename = \"Name\")]\n  pub name: Option<String>,\n\n  /// Source location of the mount.  For volumes, this contains the storage location of the volume (within `/var/lib/docker/volumes/`). For bind-mounts, and `npipe`, this contains the source (host) part of the bind-mount. For `tmpfs` mount points, this field is empty.\n  #[serde(rename = \"Source\")]\n  pub source: Option<String>,\n\n  /// Destination is the path relative to the container root (`/`) where the `Source` is mounted inside the container.\n  #[serde(rename = \"Destination\")]\n  pub destination: Option<String>,\n\n  /// Driver is the volume driver used to create the volume (if it is a volume).\n  #[serde(rename = \"Driver\")]\n  pub driver: Option<String>,\n\n  /// Mode is a comma separated list of options supplied by the user when creating the bind/volume mount.  The default is platform-specific (`\\\"z\\\"` on Linux, empty on Windows).\n  #[serde(rename = \"Mode\")]\n  pub mode: Option<String>,\n\n  /// Whether the mount is mounted writable (read-write).\n  #[serde(rename = \"RW\")]\n  pub rw: Option<bool>,\n\n  /// Propagation describes how mounts are propagated from the host into the mount point, and vice-versa. Refer to the [Linux kernel documentation](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt) for details. This field is not used on Windows.\n  #[serde(rename = \"Propagation\")]\n  pub propagation: Option<String>,\n}\n\n/// NetworkSettings exposes the network settings in the API\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct NetworkSettings {\n  /// Name of the default bridge interface when dockerd's --bridge flag is set.\n  #[serde(rename = \"Bridge\")]\n  pub bridge: Option<String>,\n\n  /// SandboxID uniquely represents a container's network stack.\n  #[serde(rename = \"SandboxID\")]\n  pub sandbox_id: Option<String>,\n\n  #[serde(default, rename = \"Ports\")]\n  pub ports: HashMap<String, Vec<PortBinding>>,\n\n  /// SandboxKey is the full path of the netns handle\n  #[serde(rename = \"SandboxKey\")]\n  pub sandbox_key: Option<String>,\n\n  /// Information about all networks that the container is connected to.\n  #[serde(default, rename = \"Networks\")]\n  pub networks: HashMap<String, EndpointSettings>,\n}\n\n/// Configuration for a network endpoint.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct EndpointSettings {\n  #[serde(rename = \"IPAMConfig\")]\n  pub ipam_config: Option<EndpointIpamConfig>,\n\n  #[serde(default, rename = \"Links\")]\n  pub links: Vec<String>,\n\n  /// MAC address for the endpoint on this network. The network driver might ignore this parameter.\n  #[serde(rename = \"MacAddress\")]\n  pub mac_address: Option<String>,\n\n  #[serde(default, rename = \"Aliases\")]\n  pub aliases: Vec<String>,\n\n  /// Unique ID of the network.\n  #[serde(rename = \"NetworkID\")]\n  pub network_id: Option<String>,\n\n  /// Unique ID for the service endpoint in a Sandbox.\n  #[serde(rename = \"EndpointID\")]\n  pub endpoint_id: Option<String>,\n\n  /// Gateway address for this network.\n  #[serde(rename = \"Gateway\")]\n  pub gateway: Option<String>,\n\n  /// IPv4 address.\n  #[serde(rename = \"IPAddress\")]\n  pub ip_address: Option<String>,\n\n  /// Mask length of the IPv4 address.\n  #[serde(rename = \"IPPrefixLen\")]\n  pub ip_prefix_len: Option<I64>,\n\n  /// IPv6 gateway address.\n  #[serde(rename = \"IPv6Gateway\")]\n  pub ipv6_gateway: Option<String>,\n\n  /// Global IPv6 address.\n  #[serde(rename = \"GlobalIPv6Address\")]\n  pub global_ipv6_address: Option<String>,\n\n  /// Mask length of the global IPv6 address.\n  #[serde(rename = \"GlobalIPv6PrefixLen\")]\n  pub global_ipv6_prefix_len: Option<I64>,\n\n  /// DriverOpts is a mapping of driver options and values. These options are passed directly to the driver and are driver specific.\n  #[serde(default, rename = \"DriverOpts\")]\n  pub driver_opts: HashMap<String, String>,\n\n  /// List of all DNS names an endpoint has on a specific network. This list is based on the container name, network aliases, container short ID, and hostname.  These DNS names are non-fully qualified but can contain several dots. You can get fully qualified DNS names by appending `.<network-name>`. For instance, if container name is `my.ctr` and the network is named `testnet`, `DNSNames` will contain `my.ctr` and the FQDN will be `my.ctr.testnet`.\n  #[serde(default, rename = \"DNSNames\")]\n  pub dns_names: Vec<String>,\n}\n\n/// EndpointIPAMConfig represents an endpoint's IPAM configuration.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct EndpointIpamConfig {\n  #[serde(rename = \"IPv4Address\")]\n  pub ipv4_address: Option<String>,\n\n  #[serde(rename = \"IPv6Address\")]\n  pub ipv6_address: Option<String>,\n\n  #[serde(default, rename = \"LinkLocalIPs\")]\n  pub link_local_ips: Vec<String>,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct ContainerStats {\n  #[serde(alias = \"Name\")]\n  pub name: String,\n  #[serde(alias = \"CPUPerc\")]\n  pub cpu_perc: String,\n  #[serde(alias = \"MemPerc\")]\n  pub mem_perc: String,\n  #[serde(alias = \"MemUsage\")]\n  pub mem_usage: String,\n  #[serde(alias = \"NetIO\")]\n  pub net_io: String,\n  #[serde(alias = \"BlockIO\")]\n  pub block_io: String,\n  #[serde(alias = \"PIDs\")]\n  pub pids: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/docker/image.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::I64;\n\nuse super::{ContainerConfig, GraphDriverData};\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ImageListItem {\n  /// The first tag in `repo_tags`, or Id if no tags.\n  pub name: String,\n  /// ID is the content-addressable ID of an image.  This identifier is a content-addressable digest calculated from the image's configuration (which includes the digests of layers used by the image).  Note that this digest differs from the `RepoDigests` below, which holds digests of image manifests that reference the image.\n  pub id: String,\n  /// ID of the parent image.  Depending on how the image was created, this field may be empty and is only set for images that were built/created locally. This field is empty if the image was pulled from an image registry.\n  pub parent_id: String,\n  /// Date and time at which the image was created as a Unix timestamp (number of seconds sinds EPOCH).\n  pub created: I64,\n  /// Total size of the image including all layers it is composed of.\n  pub size: I64,\n  /// Whether the image is in use by any container\n  pub in_use: bool,\n}\n\n/// Information about an image in the local image cache.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct Image {\n  /// ID is the content-addressable ID of an image.  This identifier is a content-addressable digest calculated from the image's configuration (which includes the digests of layers used by the image).  Note that this digest differs from the `RepoDigests` below, which holds digests of image manifests that reference the image.\n  #[serde(rename = \"Id\")]\n  pub id: Option<String>,\n\n  /// List of image names/tags in the local image cache that reference this image.  Multiple image tags can refer to the same image, and this list may be empty if no tags reference the image, in which case the image is \\\"untagged\\\", in which case it can still be referenced by its ID.\n  #[serde(default, rename = \"RepoTags\")]\n  pub repo_tags: Vec<String>,\n\n  /// List of content-addressable digests of locally available image manifests that the image is referenced from. Multiple manifests can refer to the same image.  These digests are usually only available if the image was either pulled from a registry, or if the image was pushed to a registry, which is when the manifest is generated and its digest calculated.\n  #[serde(default, rename = \"RepoDigests\")]\n  pub repo_digests: Vec<String>,\n\n  /// ID of the parent image.  Depending on how the image was created, this field may be empty and is only set for images that were built/created locally. This field is empty if the image was pulled from an image registry.\n  #[serde(rename = \"Parent\")]\n  pub parent: Option<String>,\n\n  /// Optional message that was set when committing or importing the image.\n  #[serde(rename = \"Comment\")]\n  pub comment: Option<String>,\n\n  /// Date and time at which the image was created, formatted in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.  This information is only available if present in the image, and omitted otherwise.\n  #[serde(rename = \"Created\")]\n  pub created: Option<String>,\n\n  /// The version of Docker that was used to build the image.  Depending on how the image was created, this field may be empty.\n  #[serde(rename = \"DockerVersion\")]\n  pub docker_version: Option<String>,\n\n  /// Name of the author that was specified when committing the image, or as specified through MAINTAINER (deprecated) in the Dockerfile.\n  #[serde(rename = \"Author\")]\n  pub author: Option<String>,\n\n  /// Configuration for a container that is portable between hosts.\n  #[serde(rename = \"Config\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub config: Option<ContainerConfig>,\n\n  /// Hardware CPU architecture that the image runs on.\n  #[serde(rename = \"Architecture\")]\n  pub architecture: Option<String>,\n\n  /// CPU architecture variant (presently ARM-only).\n  #[serde(rename = \"Variant\")]\n  pub variant: Option<String>,\n\n  /// Operating System the image is built to run on.\n  #[serde(rename = \"Os\")]\n  pub os: Option<String>,\n\n  /// Operating System version the image is built to run on (especially for Windows).\n  #[serde(rename = \"OsVersion\")]\n  pub os_version: Option<String>,\n\n  /// Total size of the image including all layers it is composed of.\n  #[serde(rename = \"Size\")]\n  pub size: Option<I64>,\n\n  #[serde(rename = \"GraphDriver\")]\n  pub graph_driver: Option<GraphDriverData>,\n\n  #[serde(rename = \"RootFS\")]\n  pub root_fs: Option<ImageInspectRootFs>,\n\n  #[serde(rename = \"Metadata\")]\n  pub metadata: Option<ImageInspectMetadata>,\n}\n\n/// Information about the image's RootFS, including the layer IDs.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ImageInspectRootFs {\n  #[serde(default, rename = \"Type\")]\n  pub typ: String,\n\n  #[serde(default, rename = \"Layers\")]\n  pub layers: Vec<String>,\n}\n\n/// Additional metadata of the image in the local cache. This information is local to the daemon, and not part of the image itself.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ImageInspectMetadata {\n  /// Date and time at which the image was last tagged in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.  This information is only available if the image was tagged locally, and omitted otherwise.\n  #[serde(rename = \"LastTagTime\")]\n  pub last_tag_time: Option<String>,\n}\n\n/// individual image layer information in response to ImageHistory operation\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ImageHistoryResponseItem {\n  #[serde(rename = \"Id\")]\n  pub id: String,\n\n  #[serde(rename = \"Created\")]\n  pub created: I64,\n\n  #[serde(rename = \"CreatedBy\")]\n  pub created_by: String,\n\n  #[serde(default, rename = \"Tags\")]\n  pub tags: Vec<String>,\n\n  #[serde(rename = \"Size\")]\n  pub size: I64,\n\n  #[serde(rename = \"Comment\")]\n  pub comment: String,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/docker/mod.rs",
    "content": "use std::collections::HashMap;\n\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse super::I64;\n\npub mod container;\npub mod image;\npub mod network;\npub mod stats;\npub mod volume;\n\n/// PortBinding represents a binding between a host IP address and a host port.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct PortBinding {\n  /// Host IP address that the container's port is mapped to.\n  #[serde(rename = \"HostIp\")]\n  pub host_ip: Option<String>,\n\n  /// Host port number that the container's port is mapped to.\n  #[serde(rename = \"HostPort\")]\n  pub host_port: Option<String>,\n}\n\n/// Information about the storage driver used to store the container's and image's filesystem.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct GraphDriverData {\n  /// Name of the storage driver.\n  #[serde(default, rename = \"Name\")]\n  pub name: String,\n  /// Low-level storage metadata, provided as key/value pairs.  This information is driver-specific, and depends on the storage-driver in use, and should be used for informational purposes only.\n  #[serde(default, rename = \"Data\")]\n  pub data: HashMap<String, String>,\n}\n\n/// Configuration for a container that is portable between hosts.  When used as `ContainerConfig` field in an image, `ContainerConfig` is an optional field containing the configuration of the container that was last committed when creating the image.  Previous versions of Docker builder used this field to store build cache, and it is not in active use anymore.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerConfig {\n  /// The hostname to use for the container, as a valid RFC 1123 hostname.\n  #[serde(rename = \"Hostname\")]\n  pub hostname: Option<String>,\n\n  /// The domain name to use for the container.\n  #[serde(rename = \"Domainname\")]\n  pub domainname: Option<String>,\n\n  /// The user that commands are run as inside the container.\n  #[serde(rename = \"User\")]\n  pub user: Option<String>,\n\n  /// Whether to attach to `stdin`.\n  #[serde(rename = \"AttachStdin\")]\n  pub attach_stdin: Option<bool>,\n\n  /// Whether to attach to `stdout`.\n  #[serde(rename = \"AttachStdout\")]\n  pub attach_stdout: Option<bool>,\n\n  /// Whether to attach to `stderr`.\n  #[serde(rename = \"AttachStderr\")]\n  pub attach_stderr: Option<bool>,\n\n  /// An object mapping ports to an empty object in the form:  `{\\\"<port>/<tcp|udp|sctp>\\\": {}}`\n  #[serde(default, rename = \"ExposedPorts\")]\n  pub exposed_ports: HashMap<String, HashMap<String, ()>>,\n\n  /// Attach standard streams to a TTY, including `stdin` if it is not closed.\n  #[serde(rename = \"Tty\")]\n  pub tty: Option<bool>,\n\n  /// Open `stdin`\n  #[serde(rename = \"OpenStdin\")]\n  pub open_stdin: Option<bool>,\n\n  /// Close `stdin` after one attached client disconnects\n  #[serde(rename = \"StdinOnce\")]\n  pub stdin_once: Option<bool>,\n\n  /// A list of environment variables to set inside the container in the form `[\\\"VAR=value\\\", ...]`. A variable without `=` is removed from the environment, rather than to have an empty value.\n  #[serde(default, rename = \"Env\")]\n  pub env: Vec<String>,\n\n  /// Command to run specified as a string or an array of strings.\n  #[serde(default, rename = \"Cmd\")]\n  pub cmd: Vec<String>,\n\n  #[serde(rename = \"Healthcheck\")]\n  pub healthcheck: Option<HealthConfig>,\n\n  /// Command is already escaped (Windows only)\n  #[serde(rename = \"ArgsEscaped\")]\n  pub args_escaped: Option<bool>,\n\n  /// The name (or reference) of the image to use when creating the container, or which was used when the container was created.\n  #[serde(rename = \"Image\")]\n  pub image: Option<String>,\n\n  /// An object mapping mount point paths inside the container to empty objects.\n  #[serde(default, rename = \"Volumes\")]\n  pub volumes: HashMap<String, HashMap<String, ()>>,\n\n  /// The working directory for commands to run in.\n  #[serde(rename = \"WorkingDir\")]\n  pub working_dir: Option<String>,\n\n  /// The entry point for the container as a string or an array of strings.  If the array consists of exactly one empty string (`[\\\"\\\"]`) then the entry point is reset to system default (i.e., the entry point used by docker when there is no `ENTRYPOINT` instruction in the `Dockerfile`).\n  #[serde(default, rename = \"Entrypoint\")]\n  pub entrypoint: Vec<String>,\n\n  /// Disable networking for the container.\n  #[serde(rename = \"NetworkDisabled\")]\n  pub network_disabled: Option<bool>,\n\n  /// MAC address of the container.  Deprecated: this field is deprecated in API v1.44 and up. Use EndpointSettings.MacAddress instead.\n  #[serde(rename = \"MacAddress\")]\n  pub mac_address: Option<String>,\n\n  /// `ONBUILD` metadata that were defined in the image's `Dockerfile`.\n  #[serde(default, rename = \"OnBuild\")]\n  pub on_build: Vec<String>,\n\n  /// User-defined key/value metadata.\n  #[serde(default, rename = \"Labels\")]\n  pub labels: HashMap<String, String>,\n\n  /// Signal to stop a container as a string or unsigned integer.\n  #[serde(rename = \"StopSignal\")]\n  pub stop_signal: Option<String>,\n\n  /// Timeout to stop a container in seconds.\n  #[serde(rename = \"StopTimeout\")]\n  pub stop_timeout: Option<I64>,\n\n  /// Shell for when `RUN`, `CMD`, and `ENTRYPOINT` uses a shell.\n  #[serde(default, rename = \"Shell\")]\n  pub shell: Vec<String>,\n}\n\n/// A test to perform to check that the container is healthy.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct HealthConfig {\n  /// The test to perform. Possible values are:  - `[]` inherit healthcheck from image or parent image - `[\\\"NONE\\\"]` disable healthcheck - `[\\\"CMD\\\", args...]` exec arguments directly - `[\\\"CMD-SHELL\\\", command]` run command with system's default shell\n  #[serde(default, rename = \"Test\")]\n  pub test: Vec<String>,\n\n  /// The time to wait between checks in nanoseconds. It should be 0 or at least 1000000 (1 ms). 0 means inherit.\n  #[serde(rename = \"Interval\")]\n  pub interval: Option<I64>,\n\n  /// The time to wait before considering the check to have hung. It should be 0 or at least 1000000 (1 ms). 0 means inherit.\n  #[serde(rename = \"Timeout\")]\n  pub timeout: Option<I64>,\n\n  /// The number of consecutive failures needed to consider a container as unhealthy. 0 means inherit.\n  #[serde(rename = \"Retries\")]\n  pub retries: Option<I64>,\n\n  /// Start period for the container to initialize before starting health-retries countdown in nanoseconds. It should be 0 or at least 1000000 (1 ms). 0 means inherit.\n  #[serde(rename = \"StartPeriod\")]\n  pub start_period: Option<I64>,\n\n  /// The time to wait between checks in nanoseconds during the start period. It should be 0 or at least 1000000 (1 ms). 0 means inherit.\n  #[serde(rename = \"StartInterval\")]\n  pub start_interval: Option<I64>,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/docker/network.rs",
    "content": "use std::collections::HashMap;\n\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct NetworkListItem {\n  pub name: Option<String>,\n  pub id: Option<String>,\n  pub created: Option<String>,\n  pub scope: Option<String>,\n  pub driver: Option<String>,\n  pub enable_ipv6: Option<bool>,\n  pub ipam_driver: Option<String>,\n  pub ipam_subnet: Option<String>,\n  pub ipam_gateway: Option<String>,\n  pub internal: Option<bool>,\n  pub attachable: Option<bool>,\n  pub ingress: Option<bool>,\n  /// Whether the network is attached to one or more containers\n  pub in_use: bool,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct Network {\n  #[serde(rename = \"Name\")]\n  pub name: Option<String>,\n\n  #[serde(rename = \"Id\")]\n  pub id: Option<String>,\n\n  #[serde(rename = \"Created\")]\n  pub created: Option<String>,\n\n  #[serde(rename = \"Scope\")]\n  pub scope: Option<String>,\n\n  #[serde(rename = \"Driver\")]\n  pub driver: Option<String>,\n\n  #[serde(rename = \"EnableIPv6\")]\n  pub enable_ipv6: Option<bool>,\n\n  #[serde(rename = \"IPAM\")]\n  pub ipam: Option<Ipam>,\n\n  #[serde(rename = \"Internal\")]\n  pub internal: Option<bool>,\n\n  #[serde(rename = \"Attachable\")]\n  pub attachable: Option<bool>,\n\n  #[serde(rename = \"Ingress\")]\n  pub ingress: Option<bool>,\n\n  /// This field is turned from map into array for easier usability.\n  #[serde(rename = \"Containers\")]\n  pub containers: Vec<NetworkContainer>,\n\n  #[serde(default, rename = \"Options\")]\n  pub options: HashMap<String, String>,\n\n  #[serde(default, rename = \"Labels\")]\n  pub labels: HashMap<String, String>,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct Ipam {\n  /// Name of the IPAM driver to use.\n  #[serde(rename = \"Driver\")]\n  pub driver: Option<String>,\n  /// List of IPAM configuration options, specified as a map:  ``` {\\\"Subnet\\\": <CIDR>, \\\"IPRange\\\": <CIDR>, \\\"Gateway\\\": <IP address>, \\\"AuxAddress\\\": <device_name:IP address>} ```\n  #[serde(rename = \"Config\")]\n  pub config: Vec<IpamConfig>,\n  /// Driver-specific options, specified as a map.\n  #[serde(rename = \"Options\")]\n  pub options: HashMap<String, String>,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct IpamConfig {\n  #[serde(rename = \"Subnet\")]\n  pub subnet: Option<String>,\n  #[serde(rename = \"IPRange\")]\n  pub ip_range: Option<String>,\n  #[serde(rename = \"Gateway\")]\n  pub gateway: Option<String>,\n  #[serde(rename = \"AuxiliaryAddresses\")]\n  pub auxiliary_addresses: HashMap<String, String>,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct NetworkContainer {\n  /// This is the key on the incoming map of NetworkContainer\n  #[serde(default, rename = \"ContainerID\")]\n  pub container_id: String,\n  #[serde(rename = \"Name\")]\n  pub name: Option<String>,\n  #[serde(rename = \"EndpointID\")]\n  pub endpoint_id: Option<String>,\n  #[serde(rename = \"MacAddress\")]\n  pub mac_address: Option<String>,\n  #[serde(rename = \"IPv4Address\")]\n  pub ipv4_address: Option<String>,\n  #[serde(rename = \"IPv6Address\")]\n  pub ipv6_address: Option<String>,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/docker/stats.rs",
    "content": "use std::collections::HashMap;\n\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::U64;\n\n/// Statistics sample for a container.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct FullContainerStats {\n  /// Name of the container\n  pub name: String,\n\n  /// ID of the container\n  pub id: Option<String>,\n\n  /// Date and time at which this sample was collected.\n  /// The value is formatted as [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) with nano-seconds.\n  pub read: Option<String>,\n\n  /// Date and time at which this first sample was collected.\n  /// This field is not propagated if the \\\"one-shot\\\" option is set.\n  /// If the \\\"one-shot\\\" option is set, this field may be omitted, empty,\n  /// or set to a default date (`0001-01-01T00:00:00Z`).\n  /// The value is formatted as [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) with nano-seconds.\n  pub preread: Option<String>,\n\n  /// PidsStats contains Linux-specific stats of a container's process-IDs (PIDs).\n  /// This type is Linux-specific and omitted for Windows containers.\n  pub pids_stats: Option<ContainerPidsStats>,\n\n  /// BlkioStats stores all IO service stats for data read and write.\n  /// This type is Linux-specific and holds many fields that are specific to cgroups v1.\n  /// On a cgroup v2 host, all fields other than `io_service_bytes_recursive` are omitted or `null`.\n  /// This type is only populated on Linux and omitted for Windows containers.\n  pub blkio_stats: Option<ContainerBlkioStats>,\n\n  /// The number of processors on the system.\n  /// This field is Windows-specific and always zero for Linux containers.\n  pub num_procs: Option<u32>,\n\n  #[serde(rename = \"storage_stats\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub storage_stats: Option<ContainerStorageStats>,\n\n  #[serde(rename = \"cpu_stats\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub cpu_stats: Option<ContainerCpuStats>,\n\n  #[serde(rename = \"precpu_stats\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub precpu_stats: Option<ContainerCpuStats>,\n\n  #[serde(rename = \"memory_stats\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub memory_stats: Option<ContainerMemoryStats>,\n\n  /// Network statistics for the container per interface.  This field is omitted if the container has no networking enabled.\n  #[serde(rename = \"networks\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub networks: Option<HashMap<String, ContainerNetworkStats>>,\n}\n\n/// PidsStats contains Linux-specific stats of a container's process-IDs (PIDs).  This type is Linux-specific and omitted for Windows containers.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerPidsStats {\n  /// Current is the number of PIDs in the cgroup.\n  pub current: Option<U64>,\n\n  /// Limit is the hard limit on the number of pids in the cgroup. A \\\"Limit\\\" of 0 means that there is no limit.\n  pub limit: Option<U64>,\n}\n\n/// BlkioStats stores all IO service stats for data read and write.\n/// This type is Linux-specific and holds many fields that are specific to cgroups v1.\n/// On a cgroup v2 host, all fields other than `io_service_bytes_recursive` are omitted or `null`.\n/// This type is only populated on Linux and omitted for Windows containers.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerBlkioStats {\n  #[serde(rename = \"io_service_bytes_recursive\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub io_service_bytes_recursive:\n    Option<Vec<ContainerBlkioStatEntry>>,\n\n  /// This field is only available when using Linux containers with cgroups v1.\n  /// It is omitted or `null` when using cgroups v2.\n  #[serde(rename = \"io_serviced_recursive\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub io_serviced_recursive: Option<Vec<ContainerBlkioStatEntry>>,\n\n  /// This field is only available when using Linux containers with cgroups v1.\n  /// It is omitted or `null` when using cgroups v2.\n  #[serde(rename = \"io_queue_recursive\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub io_queue_recursive: Option<Vec<ContainerBlkioStatEntry>>,\n\n  /// This field is only available when using Linux containers with cgroups v1.\n  /// It is omitted or `null` when using cgroups v2.\n  #[serde(rename = \"io_service_time_recursive\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub io_service_time_recursive: Option<Vec<ContainerBlkioStatEntry>>,\n\n  /// This field is only available when using Linux containers with cgroups v1.\n  /// It is omitted or `null` when using cgroups v2.\n  #[serde(rename = \"io_wait_time_recursive\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub io_wait_time_recursive: Option<Vec<ContainerBlkioStatEntry>>,\n\n  /// This field is only available when using Linux containers with cgroups v1.\n  /// It is omitted or `null` when using cgroups v2.\n  #[serde(rename = \"io_merged_recursive\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub io_merged_recursive: Option<Vec<ContainerBlkioStatEntry>>,\n\n  /// This field is only available when using Linux containers with cgroups v1.\n  /// It is omitted or `null` when using cgroups v2.\n  #[serde(rename = \"io_time_recursive\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub io_time_recursive: Option<Vec<ContainerBlkioStatEntry>>,\n\n  /// This field is only available when using Linux containers with cgroups v1.\n  /// It is omitted or `null` when using cgroups v2.\n  #[serde(rename = \"sectors_recursive\")]\n  #[serde(skip_serializing_if = \"Option::is_none\")]\n  pub sectors_recursive: Option<Vec<ContainerBlkioStatEntry>>,\n}\n\n/// Blkio stats entry.  This type is Linux-specific and omitted for Windows containers.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerBlkioStatEntry {\n  pub major: Option<U64>,\n  pub minor: Option<U64>,\n  pub op: Option<String>,\n  pub value: Option<U64>,\n}\n\n/// StorageStats is the disk I/O stats for read/write on Windows.\n/// This type is Windows-specific and omitted for Linux containers.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerStorageStats {\n  pub read_count_normalized: Option<U64>,\n  pub read_size_bytes: Option<U64>,\n  pub write_count_normalized: Option<U64>,\n  pub write_size_bytes: Option<U64>,\n}\n\n/// CPU related info of the container\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerCpuStats {\n  /// All CPU stats aggregated since container inception.\n  pub cpu_usage: Option<ContainerCpuUsage>,\n\n  /// System Usage.\n  /// This field is Linux-specific and omitted for Windows containers.\n  pub system_cpu_usage: Option<U64>,\n\n  /// Number of online CPUs.\n  /// This field is Linux-specific and omitted for Windows containers.\n  pub online_cpus: Option<u32>,\n\n  /// CPU throttling stats of the container.\n  /// This type is Linux-specific and omitted for Windows containers.\n  pub throttling_data: Option<ContainerThrottlingData>,\n}\n\n/// All CPU stats aggregated since container inception.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerCpuUsage {\n  /// Total CPU time consumed in nanoseconds (Linux) or 100's of nanoseconds (Windows).\n  pub total_usage: Option<U64>,\n\n  /// Total CPU time (in nanoseconds) consumed per core (Linux).\n  /// This field is Linux-specific when using cgroups v1.\n  /// It is omitted when using cgroups v2 and Windows containers.\n  pub percpu_usage: Option<Vec<U64>>,\n\n  /// Time (in nanoseconds) spent by tasks of the cgroup in kernel mode (Linux),\n  /// or time spent (in 100's of nanoseconds) by all container processes in kernel mode (Windows).\n  /// Not populated for Windows containers using Hyper-V isolation.\n  pub usage_in_kernelmode: Option<U64>,\n\n  /// Time (in nanoseconds) spent by tasks of the cgroup in user mode (Linux),\n  /// or time spent (in 100's of nanoseconds) by all container processes in kernel mode (Windows).\n  /// Not populated for Windows containers using Hyper-V isolation.\n  pub usage_in_usermode: Option<U64>,\n}\n\n/// CPU throttling stats of the container.\n/// This type is Linux-specific and omitted for Windows containers.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerThrottlingData {\n  /// Number of periods with throttling active.\n  pub periods: Option<U64>,\n\n  /// Number of periods when the container hit its throttling limit.\n  pub throttled_periods: Option<U64>,\n\n  /// Aggregated time (in nanoseconds) the container was throttled for.\n  pub throttled_time: Option<U64>,\n}\n\n/// Aggregates all memory stats since container inception on Linux.\n/// Windows returns stats for commit and private working set only.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerMemoryStats {\n  /// Current `res_counter` usage for memory.\n  /// This field is Linux-specific and omitted for Windows containers.\n  pub usage: Option<U64>,\n\n  /// Maximum usage ever recorded.\n  /// This field is Linux-specific and only supported on cgroups v1.\n  /// It is omitted when using cgroups v2 and for Windows containers.\n  pub max_usage: Option<U64>,\n\n  /// All the stats exported via memory.stat. when using cgroups v2.\n  /// This field is Linux-specific and omitted for Windows containers.\n  pub stats: Option<HashMap<String, U64>>,\n\n  /// Number of times memory usage hits limits.  This field is Linux-specific and only supported on cgroups v1. It is omitted when using cgroups v2 and for Windows containers.\n  pub failcnt: Option<U64>,\n\n  /// This field is Linux-specific and omitted for Windows containers.\n  pub limit: Option<U64>,\n\n  /// Committed bytes.\n  /// This field is Windows-specific and omitted for Linux containers.\n  pub commitbytes: Option<U64>,\n\n  /// Peak committed bytes.\n  /// This field is Windows-specific and omitted for Linux containers.\n  pub commitpeakbytes: Option<U64>,\n\n  /// Private working set.\n  /// This field is Windows-specific and omitted for Linux containers.\n  pub privateworkingset: Option<U64>,\n}\n\n/// Aggregates the network stats of one container\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ContainerNetworkStats {\n  /// Bytes received. Windows and Linux.\n  pub rx_bytes: Option<U64>,\n\n  /// Packets received. Windows and Linux.\n  pub rx_packets: Option<U64>,\n\n  /// Received errors. Not used on Windows.\n  /// This field is Linux-specific and always zero for Windows containers.\n  pub rx_errors: Option<U64>,\n\n  /// Incoming packets dropped. Windows and Linux.\n  pub rx_dropped: Option<U64>,\n\n  /// Bytes sent. Windows and Linux.\n  pub tx_bytes: Option<U64>,\n\n  /// Packets sent. Windows and Linux.\n  pub tx_packets: Option<U64>,\n\n  /// Sent errors. Not used on Windows.\n  /// This field is Linux-specific and always zero for Windows containers.\n  pub tx_errors: Option<U64>,\n\n  /// Outgoing packets dropped. Windows and Linux.\n  pub tx_dropped: Option<U64>,\n\n  /// Endpoint ID. Not used on Linux.\n  /// This field is Windows-specific and omitted for Linux containers.\n  pub endpoint_id: Option<String>,\n\n  /// Instance ID. Not used on Linux.\n  /// This field is Windows-specific and omitted for Linux containers.\n  pub instance_id: Option<String>,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/docker/volume.rs",
    "content": "use std::collections::HashMap;\n\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{I64, U64};\n\nuse super::PortBinding;\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct VolumeListItem {\n  /// The name of the volume\n  pub name: String,\n  pub driver: String,\n  pub mountpoint: String,\n  pub created: Option<String>,\n  pub scope: VolumeScopeEnum,\n  /// Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\\\"local\\\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\\\"not available\\\")\n  pub size: Option<I64>,\n  /// Whether the volume is currently attached to any container\n  pub in_use: bool,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct Volume {\n  /// Name of the volume.\n  #[serde(rename = \"Name\")]\n  pub name: String,\n\n  /// Name of the volume driver used by the volume.\n  #[serde(rename = \"Driver\")]\n  pub driver: String,\n\n  /// Mount path of the volume on the host.\n  #[serde(rename = \"Mountpoint\")]\n  pub mountpoint: String,\n\n  /// Date/Time the volume was created.\n  #[serde(rename = \"CreatedAt\")]\n  pub created_at: Option<String>,\n\n  /// Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\\\"key\\\":\\\"value\\\",\\\"key2\\\":\\\"value2\\\"}`.  The `Status` field is optional, and is omitted if the volume driver does not support this feature.\n  #[serde(default, rename = \"Status\")]\n  pub status: HashMap<String, HashMap<String, ()>>,\n\n  /// User-defined key/value metadata.\n  #[serde(default, rename = \"Labels\")]\n  pub labels: HashMap<String, String>,\n\n  /// The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level.\n  #[serde(default, rename = \"Scope\")]\n  pub scope: VolumeScopeEnum,\n\n  #[serde(rename = \"ClusterVolume\")]\n  pub cluster_volume: Option<ClusterVolume>,\n\n  /// The driver specific options used when creating the volume.\n  #[serde(default, rename = \"Options\")]\n  pub options: HashMap<String, String>,\n\n  #[serde(rename = \"UsageData\")]\n  pub usage_data: Option<VolumeUsageData>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum VolumeScopeEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"local\")]\n  Local,\n  #[serde(rename = \"global\")]\n  Global,\n}\n\n/// Options and information specific to, and only present on, Swarm CSI cluster volumes.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ClusterVolume {\n  /// The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name.\n  #[serde(rename = \"ID\")]\n  pub id: Option<String>,\n\n  #[serde(rename = \"Version\")]\n  pub version: Option<ObjectVersion>,\n\n  #[serde(rename = \"CreatedAt\")]\n  pub created_at: Option<String>,\n\n  #[serde(rename = \"UpdatedAt\")]\n  pub updated_at: Option<String>,\n\n  #[serde(rename = \"Spec\")]\n  pub spec: Option<ClusterVolumeSpec>,\n\n  #[serde(rename = \"Info\")]\n  pub info: Option<ClusterVolumeInfo>,\n\n  /// The status of the volume as it pertains to its publishing and use on specific nodes\n  #[serde(default, rename = \"PublishStatus\")]\n  pub publish_status: Vec<ClusterVolumePublishStatus>,\n}\n\n/// Information about the global status of the volume.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ClusterVolumeInfo {\n  /// The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown.\n  #[serde(rename = \"CapacityBytes\")]\n  pub capacity_bytes: Option<I64>,\n\n  /// A map of strings to strings returned from the storage plugin when the volume is created.\n  #[serde(default, rename = \"VolumeContext\")]\n  pub volume_context: HashMap<String, String>,\n\n  /// The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet.\n  #[serde(rename = \"VolumeID\")]\n  pub volume_id: Option<String>,\n\n  /// The topology this volume is actually accessible from.\n  #[serde(default, rename = \"AccessibleTopology\")]\n  pub accessible_topology: Vec<Topology>,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ClusterVolumePublishStatus {\n  /// The ID of the Swarm node the volume is published on.\n  #[serde(rename = \"NodeID\")]\n  pub node_id: Option<String>,\n\n  /// The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller.\n  #[serde(default, rename = \"State\")]\n  pub state: ClusterVolumePublishStatusStateEnum,\n\n  /// A map of strings to strings returned by the CSI controller plugin when a volume is published.\n  #[serde(default, rename = \"PublishContext\")]\n  pub publish_context: HashMap<String, String>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum ClusterVolumePublishStatusStateEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"pending-publish\")]\n  PendingPublish,\n  #[serde(rename = \"published\")]\n  Published,\n  #[serde(rename = \"pending-node-unpublish\")]\n  PendingNodeUnpublish,\n  #[serde(rename = \"pending-controller-unpublish\")]\n  PendingControllerUnpublish,\n}\n\n/// The version number of the object such as node, service, etc. This is needed to avoid conflicting writes. The client must send the version number along with the modified specification when updating these objects.  This approach ensures safe concurrency and determinism in that the change on the object may not be applied if the version number has changed from the last read. In other words, if two update requests specify the same base version, only one of the requests can succeed. As a result, two separate update requests that happen at the same time will not unintentionally overwrite each other.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ObjectVersion {\n  #[serde(rename = \"Index\")]\n  pub index: Option<U64>,\n}\n\n/// Cluster-specific options used to create the volume.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ClusterVolumeSpec {\n  /// Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services.  Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group.\n  #[serde(rename = \"Group\")]\n  pub group: Option<String>,\n\n  #[serde(rename = \"AccessMode\")]\n  pub access_mode: Option<ClusterVolumeSpecAccessMode>,\n}\n\n/// Defines how the volume is used by tasks.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ClusterVolumeSpecAccessMode {\n  /// The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time.\n  #[serde(default, rename = \"Scope\")]\n  pub scope: ClusterVolumeSpecAccessModeScopeEnum,\n\n  /// The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers.\n  #[serde(default, rename = \"Sharing\")]\n  pub sharing: ClusterVolumeSpecAccessModeSharingEnum,\n\n  /// Swarm Secrets that are passed to the CSI storage plugin when operating on this volume.\n  #[serde(default, rename = \"Secrets\")]\n  pub secrets: Vec<ClusterVolumeSpecAccessModeSecrets>,\n\n  #[serde(rename = \"AccessibilityRequirements\")]\n  pub accessibility_requirements:\n    Option<ClusterVolumeSpecAccessModeAccessibilityRequirements>,\n\n  #[serde(rename = \"CapacityRange\")]\n  pub capacity_range:\n    Option<ClusterVolumeSpecAccessModeCapacityRange>,\n\n  /// The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started.\n  #[serde(default, rename = \"Availability\")]\n  pub availability: ClusterVolumeSpecAccessModeAvailabilityEnum,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum ClusterVolumeSpecAccessModeScopeEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"single\")]\n  Single,\n  #[serde(rename = \"multi\")]\n  Multi,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum ClusterVolumeSpecAccessModeSharingEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"none\")]\n  None,\n  #[serde(rename = \"readonly\")]\n  Readonly,\n  #[serde(rename = \"onewriter\")]\n  Onewriter,\n  #[serde(rename = \"all\")]\n  All,\n}\n\n/// One cluster volume secret entry. Defines a key-value pair that is passed to the plugin.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ClusterVolumeSpecAccessModeSecrets {\n  /// Key is the name of the key of the key-value pair passed to the plugin.\n  #[serde(rename = \"Key\")]\n  pub key: Option<String>,\n\n  /// Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin.\n  #[serde(rename = \"Secret\")]\n  pub secret: Option<String>,\n}\n\n/// Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ClusterVolumeSpecAccessModeAccessibilityRequirements {\n  /// A list of required topologies, at least one of which the volume must be accessible from.\n  #[serde(default, rename = \"Requisite\")]\n  pub requisite: Vec<Topology>,\n\n  /// A list of topologies that the volume should attempt to be provisioned in.\n  #[serde(default, rename = \"Preferred\")]\n  pub preferred: Vec<Topology>,\n}\n\n#[typeshare]\npub type Topology = HashMap<String, Vec<PortBinding>>;\n\n/// The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct ClusterVolumeSpecAccessModeCapacityRange {\n  /// The volume must be at least this big. The value of 0 indicates an unspecified minimum\n  #[serde(rename = \"RequiredBytes\")]\n  pub required_bytes: Option<I64>,\n\n  /// The volume must not be bigger than this. The value of 0 indicates an unspecified maximum.\n  #[serde(rename = \"LimitBytes\")]\n  pub limit_bytes: Option<I64>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  PartialOrd,\n  Serialize,\n  Deserialize,\n  Eq,\n  Ord,\n  Default,\n)]\npub enum ClusterVolumeSpecAccessModeAvailabilityEnum {\n  #[default]\n  #[serde(rename = \"\")]\n  Empty,\n  #[serde(rename = \"active\")]\n  Active,\n  #[serde(rename = \"pause\")]\n  Pause,\n  #[serde(rename = \"drain\")]\n  Drain,\n}\n\n/// Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints.\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Serialize, Deserialize,\n)]\npub struct VolumeUsageData {\n  /// Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\\\"local\\\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\\\"not available\\\")\n  #[serde(rename = \"Size\")]\n  pub size: I64,\n\n  /// The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available.\n  #[serde(rename = \"RefCount\")]\n  pub ref_count: I64,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/logger.rs",
    "content": "use std::sync::OnceLock;\n\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct LogConfig {\n  /// The logging level. default: info\n  #[serde(default)]\n  pub level: LogLevel,\n\n  /// Controls logging to stdout / stderr\n  #[serde(default)]\n  pub stdio: StdioLogMode,\n\n  /// Use tracing-subscriber's pretty logging output option.\n  #[serde(default)]\n  pub pretty: bool,\n\n  /// Including information about the log location (ie the function which produced the log).\n  /// Tracing refers to this as the 'target'.\n  #[serde(default = \"default_location\")]\n  pub location: bool,\n\n  /// Enable opentelemetry exporting\n  #[serde(default)]\n  pub otlp_endpoint: String,\n\n  #[serde(default = \"default_opentelemetry_service_name\")]\n  pub opentelemetry_service_name: String,\n}\n\nfn default_opentelemetry_service_name() -> String {\n  String::from(\"Komodo\")\n}\n\nfn default_location() -> bool {\n  true\n}\n\nimpl Default for LogConfig {\n  fn default() -> Self {\n    Self {\n      level: Default::default(),\n      stdio: Default::default(),\n      pretty: Default::default(),\n      location: default_location(),\n      otlp_endpoint: Default::default(),\n      opentelemetry_service_name: default_opentelemetry_service_name(\n      ),\n    }\n  }\n}\n\nfn default_log_config() -> &'static LogConfig {\n  static DEFAULT_LOG_CONFIG: OnceLock<LogConfig> = OnceLock::new();\n  DEFAULT_LOG_CONFIG.get_or_init(Default::default)\n}\n\nimpl LogConfig {\n  pub fn is_default(&self) -> bool {\n    self == default_log_config()\n  }\n}\n\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  Default,\n  PartialEq,\n  Eq,\n  Hash,\n  Serialize,\n  Deserialize,\n)]\n#[serde(rename_all = \"lowercase\")]\npub enum LogLevel {\n  Trace,\n  Debug,\n  #[default]\n  Info,\n  Warn,\n  Error,\n}\n\nimpl From<LogLevel> for tracing::Level {\n  fn from(value: LogLevel) -> Self {\n    match value {\n      LogLevel::Trace => tracing::Level::TRACE,\n      LogLevel::Debug => tracing::Level::DEBUG,\n      LogLevel::Info => tracing::Level::INFO,\n      LogLevel::Warn => tracing::Level::WARN,\n      LogLevel::Error => tracing::Level::ERROR,\n    }\n  }\n}\n\nimpl From<tracing::Level> for LogLevel {\n  fn from(value: tracing::Level) -> Self {\n    match value.as_str() {\n      \"trace\" => LogLevel::Trace,\n      \"debug\" => LogLevel::Debug,\n      \"info\" => LogLevel::Info,\n      \"warn\" => LogLevel::Warn,\n      \"error\" => LogLevel::Error,\n      _ => LogLevel::Info,\n    }\n  }\n}\n\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  Default,\n  PartialEq,\n  Eq,\n  Hash,\n  Serialize,\n  Deserialize,\n)]\n#[serde(rename_all = \"lowercase\")]\npub enum StdioLogMode {\n  #[default]\n  Standard,\n  Json,\n  None,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/mod.rs",
    "content": "use std::{\n  path::{Path, PathBuf},\n  str::FromStr,\n};\n\nuse anyhow::Context;\nuse async_timing_util::unix_timestamp_ms;\nuse clap::Parser;\nuse derive_empty_traits::EmptyTraits;\nuse derive_variants::{EnumVariants, ExtractVariant};\nuse serde::{\n  Deserialize, Serialize,\n  de::{Visitor, value::MapAccessDeserializer},\n};\nuse serror::Serror;\nuse strum::{AsRefStr, Display, EnumString};\nuse typeshare::typeshare;\n\nuse crate::{\n  deserializers::file_contents_deserializer, entities::update::Log,\n  parsers::parse_key_value_list,\n};\n\n/// Subtypes of [Action][action::Action].\npub mod action;\n/// Subtypes of [Alert][alert::Alert].\npub mod alert;\n/// Subtypes of [Alerter][alerter::Alerter].\npub mod alerter;\n/// Subtypes of [ApiKey][api_key::ApiKey].\npub mod api_key;\n/// Subtypes of [Build][build::Build].\npub mod build;\n/// Subtypes of [Builder][builder::Builder].\npub mod builder;\n/// [core config][config::core] and [periphery config][config::periphery]\npub mod config;\n/// Subtypes of [Deployment][deployment::Deployment].\npub mod deployment;\n/// Networks, Images, Containers.\npub mod docker;\n/// Subtypes of [LogConfig][logger::LogConfig].\npub mod logger;\n/// Subtypes of [Permission][permission::Permission].\npub mod permission;\n/// Subtypes of [Procedure][procedure::Procedure].\npub mod procedure;\n/// Subtypes of [GitProviderAccount][provider::GitProviderAccount] and [DockerRegistryAccount][provider::DockerRegistryAccount]\npub mod provider;\n/// Subtypes of [Repo][repo::Repo].\npub mod repo;\n/// Subtypes of [Resource][resource::Resource].\npub mod resource;\n/// Subtypes of [Schedule][schedule::Schedule]\npub mod schedule;\n/// Subtypes of [Server][server::Server].\npub mod server;\n/// Subtypes of [Stack][stack::Stack]\npub mod stack;\n/// Subtypes for server stats reporting.\npub mod stats;\n/// Subtypes of [ResourceSync][sync::ResourceSync]\npub mod sync;\n/// Subtypes of [Tag][tag::Tag].\npub mod tag;\n/// Subtypes of [ResourcesToml][toml::ResourcesToml].\npub mod toml;\n/// Subtypes of [Update][update::Update].\npub mod update;\n/// Subtypes of [User][user::User].\npub mod user;\n/// Subtypes of [UserGroup][user_group::UserGroup].\npub mod user_group;\n/// Subtypes of [Variable][variable::Variable]\npub mod variable;\n\n#[typeshare(serialized_as = \"number\")]\npub type I64 = i64;\n#[typeshare(serialized_as = \"number\")]\npub type U64 = u64;\n#[typeshare(serialized_as = \"number\")]\npub type Usize = usize;\n#[typeshare(serialized_as = \"any\")]\npub type MongoDocument = bson::Document;\n#[typeshare(serialized_as = \"any\")]\npub type JsonValue = serde_json::Value;\n#[typeshare(serialized_as = \"any\")]\npub type JsonObject = serde_json::Map<String, serde_json::Value>;\n#[typeshare(serialized_as = \"MongoIdObj\")]\npub type MongoId = String;\n#[typeshare(serialized_as = \"__Serror\")]\npub type _Serror = Serror;\n\n/// Represents an empty json object: `{}`\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Default,\n  PartialEq,\n  Serialize,\n  Deserialize,\n  Parser,\n  EmptyTraits,\n)]\npub struct NoData {}\n\npub trait MergePartial: Sized {\n  type Partial;\n  fn merge_partial(self, partial: Self::Partial) -> Self;\n}\n\npub fn all_logs_success(logs: &[update::Log]) -> bool {\n  for log in logs {\n    if !log.success {\n      return false;\n    }\n  }\n  true\n}\n\npub fn optional_string(string: impl Into<String>) -> Option<String> {\n  let string = string.into();\n  if string.is_empty() {\n    None\n  } else {\n    Some(string)\n  }\n}\n\npub fn to_general_name(name: &str) -> String {\n  name.trim().replace('\\n', \"_\").to_string()\n}\n\npub fn to_path_compatible_name(name: &str) -> String {\n  name.trim().replace([' ', '\\n'], \"_\").to_string()\n}\n\n/// Enforce common container naming rules.\n/// [a-zA-Z0-9_.-]\npub fn to_container_compatible_name(name: &str) -> String {\n  name.trim().replace([' ', ',', '\\n', '&'], \"_\").to_string()\n}\n\n/// Enforce common docker naming rules, such as only lowercase, and no '.'.\n/// These apply to:\n///   - Stacks (docker project name)\n///   - Builds (docker image name)\n///   - Networks\n///   - Volumes\npub fn to_docker_compatible_name(name: &str) -> String {\n  name\n    .to_lowercase()\n    .replace([' ', '.', ',', '\\n', '&'], \"_\")\n    .trim()\n    .to_string()\n}\n\n/// Unix timestamp in milliseconds as i64\npub fn komodo_timestamp() -> i64 {\n  unix_timestamp_ms() as i64\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct MongoIdObj {\n  #[serde(rename = \"$oid\")]\n  pub oid: String,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct __Serror {\n  pub error: String,\n  pub trace: Vec<String>,\n}\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq,\n)]\npub struct SystemCommand {\n  #[serde(default)]\n  pub path: String,\n  #[serde(default, deserialize_with = \"file_contents_deserializer\")]\n  pub command: String,\n}\n\nimpl SystemCommand {\n  pub fn command(&self) -> Option<String> {\n    if self.is_none() {\n      None\n    } else {\n      Some(format!(\"cd {} && {}\", self.path, self.command))\n    }\n  }\n\n  pub fn into_option(self) -> Option<SystemCommand> {\n    if self.is_none() { None } else { Some(self) }\n  }\n\n  pub fn is_none(&self) -> bool {\n    self.command.is_empty()\n  }\n}\n\n#[typeshare]\n#[derive(Serialize, Debug, Clone, Copy, Default, PartialEq)]\npub struct Version {\n  pub major: i32,\n  pub minor: i32,\n  pub patch: i32,\n}\n\nimpl<'de> Deserialize<'de> for Version {\n  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n  where\n    D: serde::Deserializer<'de>,\n  {\n    #[derive(Deserialize)]\n    struct VersionInner {\n      major: i32,\n      minor: i32,\n      patch: i32,\n    }\n\n    impl From<VersionInner> for Version {\n      fn from(\n        VersionInner {\n          major,\n          minor,\n          patch,\n        }: VersionInner,\n      ) -> Self {\n        Version {\n          major,\n          minor,\n          patch,\n        }\n      }\n    }\n\n    struct VersionVisitor;\n\n    impl<'de> Visitor<'de> for VersionVisitor {\n      type Value = Version;\n      fn expecting(\n        &self,\n        formatter: &mut std::fmt::Formatter,\n      ) -> std::fmt::Result {\n        write!(\n          formatter,\n          \"version string or object | example: '0.2.4' or {{ \\\"major\\\": 0, \\\"minor\\\": 2, \\\"patch\\\": 4, }}\"\n        )\n      }\n\n      fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n      where\n        E: serde::de::Error,\n      {\n        v.try_into()\n          .map_err(|e| serde::de::Error::custom(format!(\"{e:#}\")))\n      }\n\n      fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>\n      where\n        A: serde::de::MapAccess<'de>,\n      {\n        Ok(\n          VersionInner::deserialize(MapAccessDeserializer::new(map))?\n            .into(),\n        )\n      }\n    }\n\n    deserializer.deserialize_any(VersionVisitor)\n  }\n}\n\nimpl std::fmt::Display for Version {\n  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n    f.write_fmt(format_args!(\n      \"{}.{}.{}\",\n      self.major, self.minor, self.patch\n    ))\n  }\n}\n\nimpl TryFrom<&str> for Version {\n  type Error = anyhow::Error;\n\n  fn try_from(value: &str) -> Result<Self, Self::Error> {\n    let mut split = value.split('.');\n    let major = split\n      .next()\n      .context(\"must provide at least major version\")?\n      .parse::<i32>()\n      .context(\"major version must be integer\")?;\n    let minor = split\n      .next()\n      .map(|minor| minor.parse::<i32>())\n      .transpose()\n      .context(\"minor version must be integer\")?\n      .unwrap_or_default();\n    let patch = split\n      .next()\n      .map(|patch| patch.parse::<i32>())\n      .transpose()\n      .context(\"patch version must be integer\")?\n      .unwrap_or_default();\n    Ok(Version {\n      major,\n      minor,\n      patch,\n    })\n  }\n}\n\nimpl Version {\n  pub fn increment(&mut self) {\n    self.patch += 1;\n  }\n\n  pub fn is_none(&self) -> bool {\n    self.major == 0 && self.minor == 0 && self.patch == 0\n  }\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize,\n)]\npub struct EnvironmentVar {\n  pub variable: String,\n  pub value: String,\n}\n\npub fn environment_vars_from_str(\n  input: &str,\n) -> anyhow::Result<Vec<EnvironmentVar>> {\n  parse_key_value_list(input).map(|list| {\n    list\n      .into_iter()\n      .map(|(variable, value)| EnvironmentVar { variable, value })\n      .collect()\n  })\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct LatestCommit {\n  pub hash: String,\n  pub message: String,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct FileContents {\n  /// The path to the file\n  pub path: String,\n  /// The contents of the file\n  pub contents: String,\n}\n\n/// Represents a scheduled maintenance window\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]\npub struct MaintenanceWindow {\n  /// Name for the maintenance window (required)\n  pub name: String,\n  /// Description of what maintenance is performed (optional)\n  #[serde(default)]\n  pub description: String,\n  /// The type of maintenance schedule:\n  ///   - Daily (default)\n  ///   - Weekly\n  ///   - OneTime\n  #[serde(default)]\n  pub schedule_type: MaintenanceScheduleType,\n  /// For Weekly schedules: Specify the day of the week (Monday, Tuesday, etc.)\n  #[serde(default)]\n  pub day_of_week: String,\n  /// For OneTime window: ISO 8601 date format (YYYY-MM-DD)\n  #[serde(default)]\n  pub date: String,\n  /// Start hour in 24-hour format (0-23) (optional, defaults to 0)\n  #[serde(default)]\n  pub hour: u8,\n  /// Start minute (0-59) (optional, defaults to 0)\n  #[serde(default)]\n  pub minute: u8,\n  /// Duration of the maintenance window in minutes (required)\n  pub duration_minutes: u32,\n  /// Timezone for maintenance window specificiation.\n  /// If empty, will use Core timezone.\n  #[serde(default)]\n  pub timezone: String,\n  /// Whether this maintenance window is currently enabled\n  #[serde(default = \"default_enabled\")]\n  pub enabled: bool,\n}\n\nfn default_enabled() -> bool {\n  true\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize,\n)]\npub enum DefaultRepoFolder {\n  /// /${root_directory}/stacks\n  Stacks,\n  /// /${root_directory}/builds\n  Builds,\n  /// /${root_directory}/repos\n  Repos,\n  /// If the repo is only cloned\n  /// in the core repo cache (resource sync),\n  /// this isn't relevant.\n  NotApplicable,\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]\npub struct RepoExecutionArgs {\n  /// Resource name (eg Build name, Repo name)\n  pub name: String,\n  /// Git provider domain. Default: `github.com`\n  pub provider: String,\n  /// Use https (vs http).\n  pub https: bool,\n  /// Configure the account used to access repo (if private)\n  pub account: Option<String>,\n  /// Full repo identifier. {namespace}/{repo_name}\n  /// Its optional to force checking and produce error if not defined.\n  pub repo: Option<String>,\n  /// Git Branch. Default: `main`\n  pub branch: String,\n  /// Specific commit hash. Optional\n  pub commit: Option<String>,\n  /// The clone destination path\n  pub destination: Option<String>,\n  /// The default folder to use.\n  /// Depends on the resource type.\n  pub default_folder: DefaultRepoFolder,\n}\n\nimpl RepoExecutionArgs {\n  pub fn path(&self, root_repo_dir: &Path) -> PathBuf {\n    match &self.destination {\n      Some(destination) => root_repo_dir\n        .join(to_path_compatible_name(&self.name))\n        .join(destination),\n      None => root_repo_dir.join(to_path_compatible_name(&self.name)),\n    }\n    .components()\n    .collect()\n  }\n\n  pub fn remote_url(\n    &self,\n    access_token: Option<&str>,\n  ) -> anyhow::Result<String> {\n    let access_token_at = match access_token {\n      Some(token) => match token.split_once(':') {\n        Some((username, token)) => format!(\n          \"{}:{}@\",\n          urlencoding::encode(username.trim()),\n          urlencoding::encode(token.trim())\n        ),\n        None => {\n          format!(\"token:{}@\", urlencoding::encode(token.trim()))\n        }\n      },\n      None => String::new(),\n    };\n    let protocol = if self.https { \"https\" } else { \"http\" };\n    let repo = self\n      .repo\n      .as_ref()\n      .context(\"resource has no repo attached\")?;\n    Ok(format!(\n      \"{protocol}://{access_token_at}{}/{repo}\",\n      self.provider\n    ))\n  }\n\n  pub fn unique_path(\n    &self,\n    repo_dir: &Path,\n  ) -> anyhow::Result<PathBuf> {\n    let repo = self\n      .repo\n      .as_ref()\n      .context(\"resource has no repo attached\")?;\n    let res = repo_dir\n      .join(self.provider.replace('/', \"-\"))\n      .join(repo.replace('/', \"-\"))\n      .join(self.branch.replace('/', \"-\"))\n      .join(self.commit.as_deref().unwrap_or(\"latest\"))\n      .components()\n      .collect();\n    Ok(res)\n  }\n}\n\nimpl From<&self::stack::Stack> for RepoExecutionArgs {\n  fn from(stack: &self::stack::Stack) -> Self {\n    RepoExecutionArgs {\n      name: stack.name.clone(),\n      provider: optional_string(&stack.config.git_provider)\n        .unwrap_or_else(|| String::from(\"github.com\")),\n      https: stack.config.git_https,\n      account: optional_string(&stack.config.git_account),\n      repo: optional_string(&stack.config.repo),\n      branch: optional_string(&stack.config.branch)\n        .unwrap_or_else(|| String::from(\"main\")),\n      commit: optional_string(&stack.config.commit),\n      destination: optional_string(&stack.config.clone_path),\n      default_folder: DefaultRepoFolder::Stacks,\n    }\n  }\n}\n\nimpl From<&self::build::Build> for RepoExecutionArgs {\n  fn from(build: &self::build::Build) -> RepoExecutionArgs {\n    RepoExecutionArgs {\n      name: build.name.clone(),\n      provider: optional_string(&build.config.git_provider)\n        .unwrap_or_else(|| String::from(\"github.com\")),\n      https: build.config.git_https,\n      account: optional_string(&build.config.git_account),\n      repo: optional_string(&build.config.repo),\n      branch: optional_string(&build.config.branch)\n        .unwrap_or_else(|| String::from(\"main\")),\n      commit: optional_string(&build.config.commit),\n      destination: None,\n      default_folder: DefaultRepoFolder::Builds,\n    }\n  }\n}\n\nimpl From<&self::repo::Repo> for RepoExecutionArgs {\n  fn from(repo: &self::repo::Repo) -> RepoExecutionArgs {\n    RepoExecutionArgs {\n      name: repo.name.clone(),\n      provider: optional_string(&repo.config.git_provider)\n        .unwrap_or_else(|| String::from(\"github.com\")),\n      https: repo.config.git_https,\n      account: optional_string(&repo.config.git_account),\n      repo: optional_string(&repo.config.repo),\n      branch: optional_string(&repo.config.branch)\n        .unwrap_or_else(|| String::from(\"main\")),\n      commit: optional_string(&repo.config.commit),\n      destination: optional_string(&repo.config.path),\n      default_folder: DefaultRepoFolder::Repos,\n    }\n  }\n}\n\nimpl From<&self::sync::ResourceSync> for RepoExecutionArgs {\n  fn from(sync: &self::sync::ResourceSync) -> Self {\n    RepoExecutionArgs {\n      name: sync.name.clone(),\n      provider: optional_string(&sync.config.git_provider)\n        .unwrap_or_else(|| String::from(\"github.com\")),\n      https: sync.config.git_https,\n      account: optional_string(&sync.config.git_account),\n      repo: optional_string(&sync.config.repo),\n      branch: optional_string(&sync.config.branch)\n        .unwrap_or_else(|| String::from(\"main\")),\n      commit: optional_string(&sync.config.commit),\n      destination: None,\n      default_folder: DefaultRepoFolder::NotApplicable,\n    }\n  }\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct RepoExecutionResponse {\n  /// Response logs\n  pub logs: Vec<Log>,\n  /// Absolute path to the repo root on the host.\n  pub path: PathBuf,\n  /// Latest short commit hash, if it could be retrieved\n  pub commit_hash: Option<String>,\n  /// Latest commit message, if it could be retrieved\n  pub commit_message: Option<String>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  Hash,\n  Default,\n  Serialize,\n  Deserialize,\n  Display,\n  EnumString,\n)]\n#[serde(rename_all = \"snake_case\")]\n#[strum(serialize_all = \"snake_case\")]\npub enum Timelength {\n  /// `1-sec`\n  #[serde(rename = \"1-sec\")]\n  #[strum(serialize = \"1-sec\")]\n  OneSecond,\n  /// `5-sec`\n  #[serde(rename = \"5-sec\")]\n  #[strum(serialize = \"5-sec\")]\n  FiveSeconds,\n  /// `10-sec`\n  #[serde(rename = \"10-sec\")]\n  #[strum(serialize = \"10-sec\")]\n  TenSeconds,\n  /// `15-sec`\n  #[serde(rename = \"15-sec\")]\n  #[strum(serialize = \"15-sec\")]\n  FifteenSeconds,\n  /// `30-sec`\n  #[serde(rename = \"30-sec\")]\n  #[strum(serialize = \"30-sec\")]\n  ThirtySeconds,\n  #[default]\n  /// `1-min`\n  #[serde(rename = \"1-min\")]\n  #[strum(serialize = \"1-min\")]\n  OneMinute,\n  /// `2-min`\n  #[serde(rename = \"2-min\")]\n  #[strum(serialize = \"2-min\")]\n  TwoMinutes,\n  /// `5-min`\n  #[serde(rename = \"5-min\")]\n  #[strum(serialize = \"5-min\")]\n  FiveMinutes,\n  /// `10-min`\n  #[serde(rename = \"10-min\")]\n  #[strum(serialize = \"10-min\")]\n  TenMinutes,\n  /// `15-min`\n  #[serde(rename = \"15-min\")]\n  #[strum(serialize = \"15-min\")]\n  FifteenMinutes,\n  /// `30-min`\n  #[serde(rename = \"30-min\")]\n  #[strum(serialize = \"30-min\")]\n  ThirtyMinutes,\n  /// `1-hr`\n  #[serde(rename = \"1-hr\")]\n  #[strum(serialize = \"1-hr\")]\n  OneHour,\n  /// `2-hr`\n  #[serde(rename = \"2-hr\")]\n  #[strum(serialize = \"2-hr\")]\n  TwoHours,\n  /// `6-hr`\n  #[serde(rename = \"6-hr\")]\n  #[strum(serialize = \"6-hr\")]\n  SixHours,\n  /// `8-hr`\n  #[serde(rename = \"8-hr\")]\n  #[strum(serialize = \"8-hr\")]\n  EightHours,\n  /// `12-hr`\n  #[serde(rename = \"12-hr\")]\n  #[strum(serialize = \"12-hr\")]\n  TwelveHours,\n  /// `1-day`\n  #[serde(rename = \"1-day\")]\n  #[strum(serialize = \"1-day\")]\n  OneDay,\n  /// `3-day`\n  #[serde(rename = \"3-day\")]\n  #[strum(serialize = \"3-day\")]\n  ThreeDay,\n  /// `1-wk`\n  #[serde(rename = \"1-wk\")]\n  #[strum(serialize = \"1-wk\")]\n  OneWeek,\n  /// `2-wk`\n  #[serde(rename = \"2-wk\")]\n  #[strum(serialize = \"2-wk\")]\n  TwoWeeks,\n  /// `30-day`\n  #[serde(rename = \"30-day\")]\n  #[strum(serialize = \"30-day\")]\n  ThirtyDays,\n}\n\nimpl TryInto<async_timing_util::Timelength> for Timelength {\n  type Error = anyhow::Error;\n  fn try_into(\n    self,\n  ) -> Result<async_timing_util::Timelength, Self::Error> {\n    async_timing_util::Timelength::from_str(&self.to_string())\n      .context(\"failed to parse timelength?\")\n  }\n}\n\n/// Days of the week\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  Default,\n  EnumString,\n  Serialize,\n  Deserialize,\n)]\npub enum DayOfWeek {\n  #[default]\n  #[serde(alias = \"monday\", alias = \"Mon\", alias = \"mon\")]\n  #[strum(serialize = \"monday\", serialize = \"Mon\", serialize = \"mon\")]\n  Monday,\n  #[serde(alias = \"tuesday\", alias = \"Tue\", alias = \"tue\")]\n  #[strum(\n    serialize = \"tuesday\",\n    serialize = \"Tue\",\n    serialize = \"tue\"\n  )]\n  Tuesday,\n  #[serde(alias = \"wednesday\", alias = \"Wed\", alias = \"wed\")]\n  #[strum(\n    serialize = \"wednesday\",\n    serialize = \"Wed\",\n    serialize = \"wed\"\n  )]\n  Wednesday,\n  #[serde(alias = \"thursday\", alias = \"Thurs\", alias = \"thurs\")]\n  #[strum(\n    serialize = \"thursday\",\n    serialize = \"Thurs\",\n    serialize = \"thurs\"\n  )]\n  Thursday,\n  #[serde(alias = \"friday\", alias = \"Fri\", alias = \"fri\")]\n  #[strum(serialize = \"friday\", serialize = \"Fri\", serialize = \"fri\")]\n  Friday,\n  #[serde(alias = \"saturday\", alias = \"Sat\", alias = \"sat\")]\n  #[strum(\n    serialize = \"saturday\",\n    serialize = \"Sat\",\n    serialize = \"sat\"\n  )]\n  Saturday,\n  #[serde(alias = \"sunday\", alias = \"Sun\", alias = \"sun\")]\n  #[strum(serialize = \"sunday\", serialize = \"Sun\", serialize = \"sun\")]\n  Sunday,\n}\n\n/// Types of maintenance schedules\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Default,\n  EnumString,\n  Serialize,\n  Deserialize,\n)]\npub enum MaintenanceScheduleType {\n  /// Daily at the specified time\n  #[default]\n  Daily,\n  /// Weekly on the specified day and time\n  Weekly,\n  /// One-time maintenance on a specific date and time\n  OneTime, // ISO 8601 date format (YYYY-MM-DD)\n}\n\n/// One representative IANA zone for each distinct base UTC offset in the tz database.\n/// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n///\n/// The `serde`/`strum` renames ensure the canonical identifier is used\n/// when serializing or parsing from a string such as `\"Etc/UTC\"`.\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Default,\n  EnumString,\n  Serialize,\n  Deserialize,\n)]\npub enum IanaTimezone {\n  /// UTC−12:00\n  #[serde(rename = \"Etc/GMT+12\")]\n  #[strum(serialize = \"Etc/GMT+12\")]\n  EtcGmtMinus12,\n\n  /// UTC−11:00\n  #[serde(rename = \"Pacific/Pago_Pago\")]\n  #[strum(serialize = \"Pacific/Pago_Pago\")]\n  PacificPagoPago,\n\n  /// UTC−10:00\n  #[serde(rename = \"Pacific/Honolulu\")]\n  #[strum(serialize = \"Pacific/Honolulu\")]\n  PacificHonolulu,\n\n  /// UTC−09:30\n  #[serde(rename = \"Pacific/Marquesas\")]\n  #[strum(serialize = \"Pacific/Marquesas\")]\n  PacificMarquesas,\n\n  /// UTC−09:00\n  #[serde(rename = \"America/Anchorage\")]\n  #[strum(serialize = \"America/Anchorage\")]\n  AmericaAnchorage,\n\n  /// UTC−08:00\n  #[serde(rename = \"America/Los_Angeles\")]\n  #[strum(serialize = \"America/Los_Angeles\")]\n  AmericaLosAngeles,\n\n  /// UTC−07:00\n  #[serde(rename = \"America/Denver\")]\n  #[strum(serialize = \"America/Denver\")]\n  AmericaDenver,\n\n  /// UTC−06:00\n  #[serde(rename = \"America/Chicago\")]\n  #[strum(serialize = \"America/Chicago\")]\n  AmericaChicago,\n\n  /// UTC−05:00\n  #[serde(rename = \"America/New_York\")]\n  #[strum(serialize = \"America/New_York\")]\n  AmericaNewYork,\n\n  /// UTC−04:00\n  #[serde(rename = \"America/Halifax\")]\n  #[strum(serialize = \"America/Halifax\")]\n  AmericaHalifax,\n\n  /// UTC−03:30\n  #[serde(rename = \"America/St_Johns\")]\n  #[strum(serialize = \"America/St_Johns\")]\n  AmericaStJohns,\n\n  /// UTC−03:00\n  #[serde(rename = \"America/Sao_Paulo\")]\n  #[strum(serialize = \"America/Sao_Paulo\")]\n  AmericaSaoPaulo,\n\n  /// UTC−02:00\n  #[serde(rename = \"America/Noronha\")]\n  #[strum(serialize = \"America/Noronha\")]\n  AmericaNoronha,\n\n  /// UTC−01:00\n  #[serde(rename = \"Atlantic/Azores\")]\n  #[strum(serialize = \"Atlantic/Azores\")]\n  AtlanticAzores,\n\n  /// UTC±00:00\n  #[default]\n  #[serde(rename = \"Etc/UTC\")]\n  #[strum(serialize = \"Etc/UTC\")]\n  EtcUtc,\n\n  /// UTC+01:00\n  #[serde(rename = \"Europe/Berlin\")]\n  #[strum(serialize = \"Europe/Berlin\")]\n  EuropeBerlin,\n\n  /// UTC+02:00\n  #[serde(rename = \"Europe/Bucharest\")]\n  #[strum(serialize = \"Europe/Bucharest\")]\n  EuropeBucharest,\n\n  /// UTC+03:00\n  #[serde(rename = \"Europe/Moscow\")]\n  #[strum(serialize = \"Europe/Moscow\")]\n  EuropeMoscow,\n\n  /// UTC+03:30\n  #[serde(rename = \"Asia/Tehran\")]\n  #[strum(serialize = \"Asia/Tehran\")]\n  AsiaTehran,\n\n  /// UTC+04:00\n  #[serde(rename = \"Asia/Dubai\")]\n  #[strum(serialize = \"Asia/Dubai\")]\n  AsiaDubai,\n\n  /// UTC+04:30\n  #[serde(rename = \"Asia/Kabul\")]\n  #[strum(serialize = \"Asia/Kabul\")]\n  AsiaKabul,\n\n  /// UTC+05:00\n  #[serde(rename = \"Asia/Karachi\")]\n  #[strum(serialize = \"Asia/Karachi\")]\n  AsiaKarachi,\n\n  /// UTC+05:30\n  #[serde(rename = \"Asia/Kolkata\")]\n  #[strum(serialize = \"Asia/Kolkata\")]\n  AsiaKolkata,\n\n  /// UTC+05:45\n  #[serde(rename = \"Asia/Kathmandu\")]\n  #[strum(serialize = \"Asia/Kathmandu\")]\n  AsiaKathmandu,\n\n  /// UTC+06:00\n  #[serde(rename = \"Asia/Dhaka\")]\n  #[strum(serialize = \"Asia/Dhaka\")]\n  AsiaDhaka,\n\n  /// UTC+06:30\n  #[serde(rename = \"Asia/Yangon\")]\n  #[strum(serialize = \"Asia/Yangon\")]\n  AsiaYangon,\n\n  /// UTC+07:00\n  #[serde(rename = \"Asia/Bangkok\")]\n  #[strum(serialize = \"Asia/Bangkok\")]\n  AsiaBangkok,\n\n  /// UTC+08:00\n  #[serde(rename = \"Asia/Shanghai\")]\n  #[strum(serialize = \"Asia/Shanghai\")]\n  AsiaShanghai,\n\n  /// UTC+08:45\n  #[serde(rename = \"Australia/Eucla\")]\n  #[strum(serialize = \"Australia/Eucla\")]\n  AustraliaEucla,\n\n  /// UTC+09:00\n  #[serde(rename = \"Asia/Tokyo\")]\n  #[strum(serialize = \"Asia/Tokyo\")]\n  AsiaTokyo,\n\n  /// UTC+09:30\n  #[serde(rename = \"Australia/Adelaide\")]\n  #[strum(serialize = \"Australia/Adelaide\")]\n  AustraliaAdelaide,\n\n  /// UTC+10:00\n  #[serde(rename = \"Australia/Sydney\")]\n  #[strum(serialize = \"Australia/Sydney\")]\n  AustraliaSydney,\n\n  /// UTC+10:30\n  #[serde(rename = \"Australia/Lord_Howe\")]\n  #[strum(serialize = \"Australia/Lord_Howe\")]\n  AustraliaLordHowe,\n\n  /// UTC+11:00\n  #[serde(rename = \"Pacific/Port_Moresby\")]\n  #[strum(serialize = \"Pacific/Port_Moresby\")]\n  PacificPortMoresby,\n\n  /// UTC+12:00\n  #[serde(rename = \"Pacific/Auckland\")]\n  #[strum(serialize = \"Pacific/Auckland\")]\n  PacificAuckland,\n\n  /// UTC+12:45\n  #[serde(rename = \"Pacific/Chatham\")]\n  #[strum(serialize = \"Pacific/Chatham\")]\n  PacificChatham,\n\n  /// UTC+13:00\n  #[serde(rename = \"Pacific/Tongatapu\")]\n  #[strum(serialize = \"Pacific/Tongatapu\")]\n  PacificTongatapu,\n\n  /// UTC+14:00\n  #[serde(rename = \"Pacific/Kiritimati\")]\n  #[strum(serialize = \"Pacific/Kiritimati\")]\n  PacificKiritimati,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  Hash,\n  Serialize,\n  Deserialize,\n  Default,\n  Display,\n  EnumString,\n  AsRefStr,\n)]\npub enum Operation {\n  // do nothing\n  #[default]\n  None,\n\n  // server\n  CreateServer,\n  UpdateServer,\n  DeleteServer,\n  RenameServer,\n  StartContainer,\n  RestartContainer,\n  PauseContainer,\n  UnpauseContainer,\n  StopContainer,\n  DestroyContainer,\n  StartAllContainers,\n  RestartAllContainers,\n  PauseAllContainers,\n  UnpauseAllContainers,\n  StopAllContainers,\n  PruneContainers,\n  CreateNetwork,\n  DeleteNetwork,\n  PruneNetworks,\n  DeleteImage,\n  PruneImages,\n  DeleteVolume,\n  PruneVolumes,\n  PruneDockerBuilders,\n  PruneBuildx,\n  PruneSystem,\n\n  // stack\n  CreateStack,\n  UpdateStack,\n  RenameStack,\n  DeleteStack,\n  WriteStackContents,\n  RefreshStackCache,\n  PullStack,\n  DeployStack,\n  StartStack,\n  RestartStack,\n  PauseStack,\n  UnpauseStack,\n  StopStack,\n  DestroyStack,\n  RunStackService,\n\n  // stack (service)\n  DeployStackService,\n  PullStackService,\n  StartStackService,\n  RestartStackService,\n  PauseStackService,\n  UnpauseStackService,\n  StopStackService,\n  DestroyStackService,\n\n  // deployment\n  CreateDeployment,\n  UpdateDeployment,\n  RenameDeployment,\n  DeleteDeployment,\n  Deploy,\n  PullDeployment,\n  StartDeployment,\n  RestartDeployment,\n  PauseDeployment,\n  UnpauseDeployment,\n  StopDeployment,\n  DestroyDeployment,\n\n  // build\n  CreateBuild,\n  UpdateBuild,\n  RenameBuild,\n  DeleteBuild,\n  RunBuild,\n  CancelBuild,\n  WriteDockerfile,\n\n  // repo\n  CreateRepo,\n  UpdateRepo,\n  RenameRepo,\n  DeleteRepo,\n  CloneRepo,\n  PullRepo,\n  BuildRepo,\n  CancelRepoBuild,\n\n  // procedure\n  CreateProcedure,\n  UpdateProcedure,\n  RenameProcedure,\n  DeleteProcedure,\n  RunProcedure,\n\n  // action\n  CreateAction,\n  UpdateAction,\n  RenameAction,\n  DeleteAction,\n  RunAction,\n\n  // builder\n  CreateBuilder,\n  UpdateBuilder,\n  RenameBuilder,\n  DeleteBuilder,\n\n  // alerter\n  CreateAlerter,\n  UpdateAlerter,\n  RenameAlerter,\n  DeleteAlerter,\n  TestAlerter,\n  SendAlert,\n\n  // sync\n  CreateResourceSync,\n  UpdateResourceSync,\n  RenameResourceSync,\n  DeleteResourceSync,\n  WriteSyncContents,\n  CommitSync,\n  RunSync,\n\n  // maintenance\n  ClearRepoCache,\n  BackupCoreDatabase,\n  GlobalAutoUpdate,\n\n  // variable\n  CreateVariable,\n  UpdateVariableValue,\n  DeleteVariable,\n\n  // git provider\n  CreateGitProviderAccount,\n  UpdateGitProviderAccount,\n  DeleteGitProviderAccount,\n\n  // docker registry\n  CreateDockerRegistryAccount,\n  UpdateDockerRegistryAccount,\n  DeleteDockerRegistryAccount,\n}\n\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Default,\n  Display,\n  EnumString,\n  PartialEq,\n  Hash,\n  Eq,\n  Clone,\n  Copy,\n)]\npub enum SearchCombinator {\n  #[default]\n  Or,\n  And,\n}\n\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  PartialEq,\n  Hash,\n  Eq,\n  Clone,\n  Copy,\n  Default,\n  Display,\n  EnumString,\n)]\n#[serde(rename_all = \"UPPERCASE\")]\n#[strum(serialize_all = \"UPPERCASE\")]\npub enum TerminationSignal {\n  #[serde(alias = \"1\")]\n  SigHup,\n  #[serde(alias = \"2\")]\n  SigInt,\n  #[serde(alias = \"3\")]\n  SigQuit,\n  #[default]\n  #[serde(alias = \"15\")]\n  SigTerm,\n}\n\n/// Used to reference a specific resource across all resource types\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  PartialEq,\n  Eq,\n  Hash,\n  Serialize,\n  Deserialize,\n  EnumVariants,\n)]\n#[variant_derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n  Hash,\n  Serialize,\n  Deserialize,\n  Display,\n  EnumString,\n  AsRefStr\n)]\n#[serde(tag = \"type\", content = \"id\")]\npub enum ResourceTarget {\n  System(String),\n  Server(String),\n  Stack(String),\n  Deployment(String),\n  Build(String),\n  Repo(String),\n  Procedure(String),\n  Action(String),\n  Builder(String),\n  Alerter(String),\n  ResourceSync(String),\n}\n\nimpl ResourceTarget {\n  pub fn system() -> ResourceTarget {\n    Self::System(\"system\".to_string())\n  }\n}\n\nimpl Default for ResourceTarget {\n  fn default() -> Self {\n    ResourceTarget::system()\n  }\n}\n\nimpl ResourceTarget {\n  pub fn is_empty(&self) -> bool {\n    match self {\n      ResourceTarget::System(id) => id.is_empty(),\n      ResourceTarget::Server(id) => id.is_empty(),\n      ResourceTarget::Stack(id) => id.is_empty(),\n      ResourceTarget::Deployment(id) => id.is_empty(),\n      ResourceTarget::Build(id) => id.is_empty(),\n      ResourceTarget::Repo(id) => id.is_empty(),\n      ResourceTarget::Procedure(id) => id.is_empty(),\n      ResourceTarget::Action(id) => id.is_empty(),\n      ResourceTarget::Builder(id) => id.is_empty(),\n      ResourceTarget::Alerter(id) => id.is_empty(),\n      ResourceTarget::ResourceSync(id) => id.is_empty(),\n    }\n  }\n\n  pub fn extract_variant_id(\n    &self,\n  ) -> (ResourceTargetVariant, &String) {\n    let id = match self {\n      ResourceTarget::System(id) => id,\n      ResourceTarget::Server(id) => id,\n      ResourceTarget::Stack(id) => id,\n      ResourceTarget::Build(id) => id,\n      ResourceTarget::Builder(id) => id,\n      ResourceTarget::Deployment(id) => id,\n      ResourceTarget::Repo(id) => id,\n      ResourceTarget::Alerter(id) => id,\n      ResourceTarget::Procedure(id) => id,\n      ResourceTarget::Action(id) => id,\n      ResourceTarget::ResourceSync(id) => id,\n    };\n    (self.extract_variant(), id)\n  }\n}\n\nimpl From<&build::Build> for ResourceTarget {\n  fn from(build: &build::Build) -> Self {\n    Self::Build(build.id.clone())\n  }\n}\n\nimpl From<&deployment::Deployment> for ResourceTarget {\n  fn from(deployment: &deployment::Deployment) -> Self {\n    Self::Deployment(deployment.id.clone())\n  }\n}\n\nimpl From<&server::Server> for ResourceTarget {\n  fn from(server: &server::Server) -> Self {\n    Self::Server(server.id.clone())\n  }\n}\n\nimpl From<&repo::Repo> for ResourceTarget {\n  fn from(repo: &repo::Repo) -> Self {\n    Self::Repo(repo.id.clone())\n  }\n}\n\nimpl From<&builder::Builder> for ResourceTarget {\n  fn from(builder: &builder::Builder) -> Self {\n    Self::Builder(builder.id.clone())\n  }\n}\n\nimpl From<&alerter::Alerter> for ResourceTarget {\n  fn from(alerter: &alerter::Alerter) -> Self {\n    Self::Alerter(alerter.id.clone())\n  }\n}\n\nimpl From<&procedure::Procedure> for ResourceTarget {\n  fn from(procedure: &procedure::Procedure) -> Self {\n    Self::Procedure(procedure.id.clone())\n  }\n}\n\nimpl From<&sync::ResourceSync> for ResourceTarget {\n  fn from(resource_sync: &sync::ResourceSync) -> Self {\n    Self::ResourceSync(resource_sync.id.clone())\n  }\n}\n\nimpl From<&stack::Stack> for ResourceTarget {\n  fn from(stack: &stack::Stack) -> Self {\n    Self::Stack(stack.id.clone())\n  }\n}\n\nimpl From<&action::Action> for ResourceTarget {\n  fn from(action: &action::Action) -> Self {\n    Self::Action(action.id.clone())\n  }\n}\n\nimpl ResourceTargetVariant {\n  /// These need to use snake case\n  pub fn toml_header(&self) -> &'static str {\n    match self {\n      ResourceTargetVariant::System => \"system\",\n      ResourceTargetVariant::Build => \"build\",\n      ResourceTargetVariant::Builder => \"builder\",\n      ResourceTargetVariant::Deployment => \"deployment\",\n      ResourceTargetVariant::Server => \"server\",\n      ResourceTargetVariant::Repo => \"repo\",\n      ResourceTargetVariant::Alerter => \"alerter\",\n      ResourceTargetVariant::Procedure => \"procedure\",\n      ResourceTargetVariant::ResourceSync => \"resource_sync\",\n      ResourceTargetVariant::Stack => \"stack\",\n      ResourceTargetVariant::Action => \"action\",\n    }\n  }\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,\n)]\npub enum ScheduleFormat {\n  #[default]\n  English,\n  Cron,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,\n)]\n#[serde(rename_all = \"snake_case\")]\npub enum FileFormat {\n  #[default]\n  KeyValue,\n  Toml,\n  Yaml,\n  Json,\n}\n\n/// Used with ExecuteTerminal to capture the exit code\npub const KOMODO_EXIT_CODE: &str = \"__KOMODO_EXIT_CODE:\";\n\npub fn resource_link(\n  host: &str,\n  resource_type: ResourceTargetVariant,\n  id: &str,\n) -> String {\n  let path = match resource_type {\n    ResourceTargetVariant::System => unreachable!(),\n    ResourceTargetVariant::Build => format!(\"/builds/{id}\"),\n    ResourceTargetVariant::Builder => {\n      format!(\"/builders/{id}\")\n    }\n    ResourceTargetVariant::Deployment => {\n      format!(\"/deployments/{id}\")\n    }\n    ResourceTargetVariant::Stack => {\n      format!(\"/stacks/{id}\")\n    }\n    ResourceTargetVariant::Server => {\n      format!(\"/servers/{id}\")\n    }\n    ResourceTargetVariant::Repo => format!(\"/repos/{id}\"),\n    ResourceTargetVariant::Alerter => {\n      format!(\"/alerters/{id}\")\n    }\n    ResourceTargetVariant::Procedure => {\n      format!(\"/procedures/{id}\")\n    }\n    ResourceTargetVariant::Action => {\n      format!(\"/actions/{id}\")\n    }\n    ResourceTargetVariant::ResourceSync => {\n      format!(\"/resource-syncs/{id}\")\n    }\n  };\n  format!(\"{host}{path}\")\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/permission.rs",
    "content": "use std::fmt::Write;\n\nuse derive_variants::EnumVariants;\nuse indexmap::IndexSet;\nuse serde::{Deserialize, Serialize};\nuse strum::{\n  AsRefStr, Display, EnumString, IntoStaticStr, VariantArray,\n};\nuse typeshare::typeshare;\n\nuse super::{MongoId, ResourceTarget};\n\n/// Representation of a User or UserGroups permission on a resource.\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\n// To query for all permissions on user target\n#[cfg_attr(feature = \"mongo\", doc_index({ \"user_target.type\": 1, \"user_target.id\": 1 }))]\n// To query for all permissions on a resource target\n#[cfg_attr(feature = \"mongo\", doc_index({ \"resource_target.type\": 1, \"resource_target.id\": 1 }))]\n// Only one permission allowed per user / resource target\n#[cfg_attr(feature = \"mongo\", unique_doc_index({\n  \"user_target.type\": 1,\n  \"user_target.id\": 1,\n  \"resource_target.type\": 1,\n  \"resource_target.id\": 1\n}))]\npub struct Permission {\n  /// The id of the permission document\n  #[serde(\n    default,\n    rename = \"_id\",\n    skip_serializing_if = \"String::is_empty\",\n    with = \"bson::serde_helpers::hex_string_as_object_id\"\n  )]\n  pub id: MongoId,\n  /// The target User / UserGroup\n  pub user_target: UserTarget,\n  /// The target resource\n  pub resource_target: ResourceTarget,\n  /// The permission level for the [user_target] on the [resource_target].\n  #[serde(default)]\n  pub level: PermissionLevel,\n  /// Any specific permissions for the [user_target] on the [resource_target].\n  #[serde(default)]\n  pub specific: IndexSet<SpecificPermission>,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize, EnumVariants)]\n#[variant_derive(\n  Debug,\n  Clone,\n  Copy,\n  Serialize,\n  Deserialize,\n  AsRefStr\n)]\n#[serde(tag = \"type\", content = \"id\")]\npub enum UserTarget {\n  /// User Id\n  User(String),\n  /// UserGroup Id\n  UserGroup(String),\n}\n\nimpl UserTarget {\n  pub fn extract_variant_id(self) -> (UserTargetVariant, String) {\n    match self {\n      UserTarget::User(id) => (UserTargetVariant::User, id),\n      UserTarget::UserGroup(id) => (UserTargetVariant::UserGroup, id),\n    }\n  }\n}\n\n/// The levels of permission that a User or UserGroup can have on a resource.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Display,\n  EnumString,\n  AsRefStr,\n  Hash,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n  Default,\n)]\npub enum PermissionLevel {\n  /// No permissions.\n  #[default]\n  None,\n  /// Can read resource information and config\n  Read,\n  /// Can execute actions on the resource\n  Execute,\n  /// Can update the resource configuration\n  Write,\n}\n\nimpl Default for &PermissionLevel {\n  fn default() -> Self {\n    &PermissionLevel::None\n  }\n}\n\n/// The specific types of permission that a User or UserGroup can have on a resource.\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Display,\n  EnumString,\n  AsRefStr,\n  IntoStaticStr,\n  VariantArray,\n  Hash,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n)]\npub enum SpecificPermission {\n  /// On **Server**\n  ///   - Access the terminal apis\n  /// On **Stack / Deployment**\n  ///   - Access the container exec Apis\n  Terminal,\n  /// On **Server**\n  ///   - Allowed to attach Stacks, Deployments, Repos, Builders to the Server\n  /// On **Builder**\n  ///   - Allowed to attach Builds to the Builder\n  /// On **Build**\n  ///   - Allowed to attach Deployments to the Build\n  Attach,\n  /// On **Server**\n  ///   - Access the `container inspect` apis\n  /// On **Stack / Deployment**\n  ///   - Access `container inspect` apis for associated containers\n  Inspect,\n  /// On **Server**\n  ///   - Read all container logs on the server\n  /// On **Stack / Deployment**\n  ///   - Read the container logs\n  Logs,\n  /// On **Server**\n  ///   - Read all the processes on the host\n  Processes,\n}\n\nimpl SpecificPermission {\n  fn all() -> IndexSet<SpecificPermission> {\n    SpecificPermission::VARIANTS.iter().cloned().collect()\n  }\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Default)]\npub struct PermissionLevelAndSpecifics {\n  pub level: PermissionLevel,\n  pub specific: IndexSet<SpecificPermission>,\n}\n\nimpl From<PermissionLevel> for PermissionLevelAndSpecifics {\n  fn from(level: PermissionLevel) -> Self {\n    Self {\n      level,\n      specific: IndexSet::new(),\n    }\n  }\n}\n\nimpl From<&Permission> for PermissionLevelAndSpecifics {\n  fn from(value: &Permission) -> Self {\n    Self {\n      level: value.level,\n      specific: value.specific.clone(),\n    }\n  }\n}\n\nimpl PermissionLevel {\n  /// Add all possible permissions (for use in admin case)\n  pub fn all(self) -> PermissionLevelAndSpecifics {\n    PermissionLevelAndSpecifics {\n      level: self,\n      specific: SpecificPermission::all(),\n    }\n  }\n\n  pub fn specifics(\n    self,\n    specific: IndexSet<SpecificPermission>,\n  ) -> PermissionLevelAndSpecifics {\n    PermissionLevelAndSpecifics {\n      level: self,\n      specific,\n    }\n  }\n\n  fn specific(\n    self,\n    specific: SpecificPermission,\n  ) -> PermissionLevelAndSpecifics {\n    PermissionLevelAndSpecifics {\n      level: self,\n      specific: [specific].into_iter().collect(),\n    }\n  }\n\n  /// Operation requires Terminal permission\n  pub fn terminal(self) -> PermissionLevelAndSpecifics {\n    self.specific(SpecificPermission::Terminal)\n  }\n\n  /// Operation requires Attach permission\n  pub fn attach(self) -> PermissionLevelAndSpecifics {\n    self.specific(SpecificPermission::Attach)\n  }\n\n  /// Operation requires Inspect permission\n  pub fn inspect(self) -> PermissionLevelAndSpecifics {\n    self.specific(SpecificPermission::Inspect)\n  }\n\n  /// Operation requires Logs permission\n  pub fn logs(self) -> PermissionLevelAndSpecifics {\n    self.specific(SpecificPermission::Logs)\n  }\n\n  /// Operation requires Processes permission\n  pub fn processes(self) -> PermissionLevelAndSpecifics {\n    self.specific(SpecificPermission::Processes)\n  }\n}\n\nimpl PermissionLevelAndSpecifics {\n  /// Returns true when self.level >= other.level,\n  /// and has all required specific permissions.\n  pub fn fulfills(\n    &self,\n    other: &PermissionLevelAndSpecifics,\n  ) -> bool {\n    if self.level < other.level {\n      return false;\n    }\n    for specific in other.specific.iter() {\n      if !self.specific.contains(specific) {\n        return false;\n      }\n    }\n    true\n  }\n\n  /// Returns true when self has all required specific permissions.\n  pub fn fulfills_specific(\n    &self,\n    specifics: &IndexSet<SpecificPermission>,\n  ) -> bool {\n    for specific in specifics.iter() {\n      if !self.specific.contains(specific) {\n        return false;\n      }\n    }\n    true\n  }\n\n  pub fn specifics_for_log(&self) -> String {\n    let mut res = String::new();\n    for specific in self.specific.iter() {\n      if res.is_empty() {\n        write!(&mut res, \"{specific}\").unwrap();\n      } else {\n        write!(&mut res, \", {specific}\").unwrap();\n      }\n    }\n    res\n  }\n\n  pub fn specifics(\n    mut self,\n    specific: IndexSet<SpecificPermission>,\n  ) -> PermissionLevelAndSpecifics {\n    self.specific = specific;\n    self\n  }\n\n  fn specific(\n    mut self,\n    specific: SpecificPermission,\n  ) -> PermissionLevelAndSpecifics {\n    self.specific.insert(specific);\n    PermissionLevelAndSpecifics {\n      level: self.level,\n      specific: self.specific,\n    }\n  }\n\n  /// Operation requires Terminal permission\n  pub fn terminal(self) -> PermissionLevelAndSpecifics {\n    self.specific(SpecificPermission::Terminal)\n  }\n\n  /// Operation requires Attach permission\n  pub fn attach(self) -> PermissionLevelAndSpecifics {\n    self.specific(SpecificPermission::Attach)\n  }\n\n  /// Operation requires Inspect permission\n  pub fn inspect(self) -> PermissionLevelAndSpecifics {\n    self.specific(SpecificPermission::Inspect)\n  }\n\n  /// Operation requires Logs permission\n  pub fn logs(self) -> PermissionLevelAndSpecifics {\n    self.specific(SpecificPermission::Logs)\n  }\n\n  /// Operation requires Processes permission\n  pub fn processes(self) -> PermissionLevelAndSpecifics {\n    self.specific(SpecificPermission::Processes)\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/procedure.rs",
    "content": "use bson::Document;\nuse derive_builder::Builder;\nuse derive_default_builder::DefaultBuilder;\nuse partial_derive2::Partial;\nuse serde::{Deserialize, Serialize};\nuse strum::Display;\nuse typeshare::typeshare;\n\nuse crate::api::execute::Execution;\n\nuse super::{\n  I64, ScheduleFormat,\n  resource::{Resource, ResourceListItem, ResourceQuery},\n};\n\n#[typeshare]\npub type ProcedureListItem = ResourceListItem<ProcedureListItemInfo>;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ProcedureListItemInfo {\n  /// Number of stages procedure has.\n  pub stages: I64,\n  /// Reflect whether last run successful / currently running.\n  pub state: ProcedureState,\n  /// Procedure last successful run timestamp in ms.\n  pub last_run_at: Option<I64>,\n  /// If the procedure has schedule enabled, this is the\n  /// next scheduled run time in unix ms.\n  pub next_scheduled_run: Option<I64>,\n  /// If there is an error parsing schedule expression,\n  /// it will be given here.\n  pub schedule_error: Option<String>,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  Default,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n  Serialize,\n  Deserialize,\n  Display,\n)]\npub enum ProcedureState {\n  /// Currently running\n  Running,\n  /// Last run successful\n  Ok,\n  /// Last run failed\n  Failed,\n  /// Other case (never run)\n  #[default]\n  Unknown,\n}\n\n/// Procedures run a series of stages sequentially, where\n/// each stage runs executions in parallel.\n#[typeshare]\npub type Procedure = Resource<ProcedureConfig, ()>;\n\n#[typeshare(serialized_as = \"Partial<ProcedureConfig>\")]\npub type _PartialProcedureConfig = PartialProcedureConfig;\n\n/// Config for the [Procedure]\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize, Partial, Builder)]\n#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)]\n#[partial(skip_serializing_none, from, diff)]\npub struct ProcedureConfig {\n  /// The stages to be run by the procedure.\n  #[serde(default, alias = \"stage\")]\n  #[partial_attr(serde(alias = \"stage\"))]\n  #[builder(default)]\n  pub stages: Vec<ProcedureStage>,\n\n  /// Choose whether to specify schedule as regular CRON, or using the english to CRON parser.\n  #[serde(default)]\n  #[builder(default)]\n  pub schedule_format: ScheduleFormat,\n\n  /// Optionally provide a schedule for the procedure to run on.\n  ///\n  /// There are 2 ways to specify a schedule:\n  ///\n  /// 1. Regular CRON expression:\n  ///\n  /// (second, minute, hour, day, month, day-of-week)\n  /// ```text\n  /// 0 0 0 1,15 * ?\n  /// ```\n  ///\n  /// 2. \"English\" expression via [english-to-cron](https://crates.io/crates/english-to-cron):\n  ///\n  /// ```text\n  /// at midnight on the 1st and 15th of the month\n  /// ```\n  #[serde(default)]\n  #[builder(default)]\n  pub schedule: String,\n\n  /// Whether schedule is enabled if one is provided.\n  /// Can be used to temporarily disable the schedule.\n  #[serde(default = \"default_schedule_enabled\")]\n  #[builder(default = \"default_schedule_enabled()\")]\n  #[partial_default(default_schedule_enabled())]\n  pub schedule_enabled: bool,\n\n  /// Optional. A TZ Identifier. If not provided, will use Core local timezone.\n  /// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n  #[serde(default)]\n  #[builder(default)]\n  pub schedule_timezone: String,\n\n  /// Whether to send alerts when the schedule was run.\n  #[serde(default = \"default_schedule_alert\")]\n  #[builder(default = \"default_schedule_alert()\")]\n  #[partial_default(default_schedule_alert())]\n  pub schedule_alert: bool,\n\n  /// Whether to send alerts when this procedure fails.\n  #[serde(default = \"default_failure_alert\")]\n  #[builder(default = \"default_failure_alert()\")]\n  #[partial_default(default_failure_alert())]\n  pub failure_alert: bool,\n\n  /// Whether incoming webhooks actually trigger action.\n  #[serde(default = \"default_webhook_enabled\")]\n  #[builder(default = \"default_webhook_enabled()\")]\n  #[partial_default(default_webhook_enabled())]\n  pub webhook_enabled: bool,\n\n  /// Optionally provide an alternate webhook secret for this procedure.\n  /// If its an empty string, use the default secret from the config.\n  #[serde(default)]\n  #[builder(default)]\n  pub webhook_secret: String,\n}\n\nimpl ProcedureConfig {\n  pub fn builder() -> ProcedureConfigBuilder {\n    ProcedureConfigBuilder::default()\n  }\n}\n\nfn default_schedule_enabled() -> bool {\n  true\n}\n\nfn default_schedule_alert() -> bool {\n  true\n}\n\nfn default_failure_alert() -> bool {\n  true\n}\n\nfn default_webhook_enabled() -> bool {\n  true\n}\n\nimpl Default for ProcedureConfig {\n  fn default() -> Self {\n    Self {\n      stages: Default::default(),\n      schedule_format: Default::default(),\n      schedule: Default::default(),\n      schedule_enabled: default_schedule_enabled(),\n      schedule_timezone: Default::default(),\n      schedule_alert: default_schedule_alert(),\n      failure_alert: default_failure_alert(),\n      webhook_enabled: default_webhook_enabled(),\n      webhook_secret: Default::default(),\n    }\n  }\n}\n\n/// A single stage of a procedure. Runs a list of executions in parallel.\n#[typeshare]\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct ProcedureStage {\n  /// A name for the procedure\n  pub name: String,\n  /// Whether the stage should be run as part of the procedure.\n  #[serde(default = \"default_enabled\")]\n  pub enabled: bool,\n  /// The executions in the stage\n  #[serde(default, alias = \"execution\")]\n  pub executions: Vec<EnabledExecution>,\n}\n\n/// Allows to enable / disabled procedures in the sequence / parallel vec on the fly\n#[typeshare]\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct EnabledExecution {\n  /// The execution request to run.\n  pub execution: Execution,\n  /// Whether the execution is enabled to run in the procedure.\n  #[serde(default = \"default_enabled\")]\n  pub enabled: bool,\n}\n\nfn default_enabled() -> bool {\n  true\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]\npub struct ProcedureActionState {\n  pub running: bool,\n}\n\n// QUERY\n\n#[typeshare]\npub type ProcedureQuery = ResourceQuery<ProcedureQuerySpecifics>;\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,\n)]\npub struct ProcedureQuerySpecifics {}\n\nimpl super::resource::AddFilters for ProcedureQuerySpecifics {\n  fn add_filters(&self, _: &mut Document) {}\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/provider.rs",
    "content": "use partial_derive2::Partial;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse super::MongoId;\n\n#[typeshare(serialized_as = \"Partial<GitProviderAccount>\")]\npub type _PartialGitProviderAccount = PartialGitProviderAccount;\n\n/// Configuration to access private git repos from various git providers.\n/// Note. Cannot create two accounts with the same domain and username.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default, Partial)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[partial(skip_serializing_none, from, diff)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\n#[cfg_attr(feature = \"mongo\", unique_doc_index({ \"domain\": 1, \"username\": 1 }))]\npub struct GitProviderAccount {\n  /// The Mongo ID of the git provider account.\n  /// This field is de/serialized from/to JSON as\n  /// `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized User) }`\n  #[serde(\n    default,\n    rename = \"_id\",\n    skip_serializing_if = \"String::is_empty\",\n    with = \"bson::serde_helpers::hex_string_as_object_id\"\n  )]\n  pub id: MongoId,\n  /// The domain of the provider.\n  ///\n  /// For git, this cannot include the protocol eg 'http://',\n  /// which is controlled with 'https' field.\n  #[cfg_attr(feature = \"mongo\", index)]\n  #[serde(default = \"default_git_domain\")]\n  #[partial_default(default_git_domain())]\n  pub domain: String,\n  /// Whether git provider is accessed over http or https.\n  #[serde(default = \"default_https\")]\n  #[partial_default(default_https())]\n  pub https: bool,\n  /// The account username\n  #[cfg_attr(feature = \"mongo\", index)]\n  #[serde(default)]\n  pub username: String,\n  /// The token in plain text on the db.\n  /// If the database / host can be accessed this is insecure.\n  #[serde(default)]\n  pub token: String,\n}\n\nfn default_git_domain() -> String {\n  String::from(\"github.com\")\n}\n\nfn default_https() -> bool {\n  true\n}\n\n#[typeshare(serialized_as = \"Partial<DockerRegistryAccount>\")]\npub type _PartialDockerRegistryAccount = PartialDockerRegistryAccount;\n\n/// Configuration to access private image repositories on various registries.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default, Partial)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[partial(skip_serializing_none, from, diff)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\n#[cfg_attr(feature = \"mongo\", unique_doc_index({ \"domain\": 1, \"username\": 1 }))]\npub struct DockerRegistryAccount {\n  /// The Mongo ID of the docker registry account.\n  /// This field is de/serialized from/to JSON as\n  /// `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of DockerRegistryAccount) }`\n  #[serde(\n    default,\n    rename = \"_id\",\n    skip_serializing_if = \"String::is_empty\",\n    with = \"bson::serde_helpers::hex_string_as_object_id\"\n  )]\n  pub id: MongoId,\n  /// The domain of the provider.\n  ///\n  /// For docker registry, this can include 'http://...',\n  /// however this is not recommended and won't work unless \"insecure registries\" are enabled\n  /// on your hosts. See <https://docs.docker.com/reference/cli/dockerd/#insecure-registries>.\n  #[cfg_attr(feature = \"mongo\", index)]\n  #[serde(default = \"default_registry_domain\")]\n  #[partial_default(default_registry_domain())]\n  pub domain: String,\n  /// The account username\n  #[cfg_attr(feature = \"mongo\", index)]\n  #[serde(default)]\n  pub username: String,\n  /// The token in plain text on the db.\n  /// If the database / host can be accessed this is insecure.\n  #[serde(default)]\n  pub token: String,\n}\n\nfn default_registry_domain() -> String {\n  String::from(\"docker.io\")\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/repo.rs",
    "content": "use anyhow::Context;\nuse bson::{Document, doc};\nuse derive_builder::Builder;\nuse derive_default_builder::DefaultBuilder;\nuse partial_derive2::Partial;\nuse serde::{Deserialize, Serialize};\nuse strum::Display;\nuse typeshare::typeshare;\n\nuse crate::{\n  deserializers::{\n    env_vars_deserializer, option_env_vars_deserializer,\n    option_string_list_deserializer, string_list_deserializer,\n  },\n  entities::I64,\n};\n\nuse super::{\n  EnvironmentVar, SystemCommand, environment_vars_from_str,\n  resource::{Resource, ResourceListItem, ResourceQuery},\n};\n\n#[typeshare]\npub type RepoListItem = ResourceListItem<RepoListItemInfo>;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct RepoListItemInfo {\n  /// The server that repo sits on.\n  pub server_id: String,\n  /// The builder that builds the repo.\n  pub builder_id: String,\n  /// Repo last cloned / pulled timestamp in ms.\n  pub last_pulled_at: I64,\n  /// Repo last built timestamp in ms.\n  pub last_built_at: I64,\n  /// The git provider domain\n  pub git_provider: String,\n  /// The configured repo\n  pub repo: String,\n  /// The configured branch\n  pub branch: String,\n  /// Full link to the repo.\n  pub repo_link: String,\n  /// The repo state\n  pub state: RepoState,\n  /// If the repo is cloned, will be the cloned short commit hash.\n  pub cloned_hash: Option<String>,\n  /// If the repo is cloned, will be the cloned commit message.\n  pub cloned_message: Option<String>,\n  /// If the repo is built, will be the latest built short commit hash.\n  pub built_hash: Option<String>,\n  /// Will be the latest remote short commit hash.\n  pub latest_hash: Option<String>,\n}\n\n#[typeshare]\n#[derive(\n  Debug, Clone, Copy, Default, Serialize, Deserialize, Display,\n)]\npub enum RepoState {\n  /// Unknown case\n  #[default]\n  Unknown,\n  /// Last clone / pull successful (or never cloned)\n  Ok,\n  /// Last clone / pull failed\n  Failed,\n  /// Currently cloning\n  Cloning,\n  /// Currently pulling\n  Pulling,\n  /// Currently building\n  Building,\n}\n\n#[typeshare]\npub type Repo = Resource<RepoConfig, RepoInfo>;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct RepoInfo {\n  /// When repo was last pulled\n  #[serde(default)]\n  pub last_pulled_at: I64,\n  /// When repo was last built\n  #[serde(default)]\n  pub last_built_at: I64,\n  /// Latest built short commit hash, or null.\n  pub built_hash: Option<String>,\n  /// Latest built commit message, or null. Only for repo based stacks\n  pub built_message: Option<String>,\n  /// Latest remote short commit hash, or null.\n  pub latest_hash: Option<String>,\n  /// Latest remote commit message, or null\n  pub latest_message: Option<String>,\n}\n\n#[typeshare(serialized_as = \"Partial<RepoConfig>\")]\npub type _PartialRepoConfig = PartialRepoConfig;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[partial(skip_serializing_none, from, diff)]\npub struct RepoConfig {\n  /// The server to clone the repo on.\n  #[serde(default, alias = \"server\")]\n  #[partial_attr(serde(alias = \"server\"))]\n  #[builder(default)]\n  pub server_id: String,\n\n  /// Attach a builder to 'build' the repo.\n  #[serde(default, alias = \"builder\")]\n  #[partial_attr(serde(alias = \"builder\"))]\n  #[builder(default)]\n  pub builder_id: String,\n\n  /// The git provider domain. Default: github.com\n  #[serde(default = \"default_git_provider\")]\n  #[builder(default = \"default_git_provider()\")]\n  #[partial_default(default_git_provider())]\n  pub git_provider: String,\n\n  /// Whether to use https to clone the repo (versus http). Default: true\n  ///\n  /// Note. Komodo does not currently support cloning repos via ssh.\n  #[serde(default = \"default_git_https\")]\n  #[builder(default = \"default_git_https()\")]\n  #[partial_default(default_git_https())]\n  pub git_https: bool,\n\n  /// The git account used to access private repos.\n  /// Passing empty string can only clone public repos.\n  ///\n  /// Note. A token for the account must be available in the core config or the builder server's periphery config\n  /// for the configured git provider.\n  #[serde(default)]\n  #[builder(default)]\n  pub git_account: String,\n\n  /// The github repo to clone.\n  #[serde(default)]\n  #[builder(default)]\n  pub repo: String,\n\n  /// The repo branch.\n  #[serde(default = \"default_branch\")]\n  #[builder(default = \"default_branch()\")]\n  #[partial_default(default_branch())]\n  pub branch: String,\n\n  /// Optionally set a specific commit hash.\n  #[serde(default)]\n  #[builder(default)]\n  pub commit: String,\n\n  /// Explicitly specify the folder to clone the repo in.\n  /// - If absolute (has leading '/')\n  ///   - Used directly as the path\n  /// - If relative\n  ///   - Taken relative to Periphery `repo_dir` (ie `${root_directory}/repos`)\n  #[serde(default)]\n  #[builder(default)]\n  pub path: String,\n\n  /// Whether incoming webhooks actually trigger action.\n  #[serde(default = \"default_webhook_enabled\")]\n  #[builder(default = \"default_webhook_enabled()\")]\n  #[partial_default(default_webhook_enabled())]\n  pub webhook_enabled: bool,\n\n  /// Optionally provide an alternate webhook secret for this repo.\n  /// If its an empty string, use the default secret from the config.\n  #[serde(default)]\n  #[builder(default)]\n  pub webhook_secret: String,\n\n  /// Command to be run after the repo is cloned.\n  /// The path is relative to the root of the repo.\n  #[serde(default)]\n  #[builder(default)]\n  pub on_clone: SystemCommand,\n\n  /// Command to be run after the repo is pulled.\n  /// The path is relative to the root of the repo.\n  #[serde(default)]\n  #[builder(default)]\n  pub on_pull: SystemCommand,\n\n  /// Configure quick links that are displayed in the resource header\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub links: Vec<String>,\n\n  /// The environment variables passed to the compose file.\n  /// They will be written to path defined in env_file_path,\n  /// which is given relative to the run directory.\n  ///\n  /// If it is empty, no file will be written.\n  #[serde(default, deserialize_with = \"env_vars_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_env_vars_deserializer\"\n  ))]\n  #[builder(default)]\n  pub environment: String,\n\n  /// The name of the written environment file before `docker compose up`.\n  /// Relative to the repo root.\n  /// Default: .env\n  #[serde(default = \"default_env_file_path\")]\n  #[builder(default = \"default_env_file_path()\")]\n  #[partial_default(default_env_file_path())]\n  pub env_file_path: String,\n\n  /// Whether to skip secret interpolation into the repo environment variable file.\n  #[serde(default)]\n  #[builder(default)]\n  pub skip_secret_interp: bool,\n}\n\nimpl RepoConfig {\n  pub fn builder() -> RepoConfigBuilder {\n    RepoConfigBuilder::default()\n  }\n\n  pub fn env_vars(&self) -> anyhow::Result<Vec<EnvironmentVar>> {\n    environment_vars_from_str(&self.environment)\n      .context(\"Invalid environment\")\n  }\n}\n\nfn default_git_provider() -> String {\n  String::from(\"github.com\")\n}\n\nfn default_git_https() -> bool {\n  true\n}\n\nfn default_branch() -> String {\n  String::from(\"main\")\n}\n\nfn default_env_file_path() -> String {\n  String::from(\".env\")\n}\n\nfn default_webhook_enabled() -> bool {\n  true\n}\n\nimpl Default for RepoConfig {\n  fn default() -> Self {\n    Self {\n      server_id: Default::default(),\n      builder_id: Default::default(),\n      git_provider: default_git_provider(),\n      git_https: default_git_https(),\n      repo: Default::default(),\n      branch: default_branch(),\n      commit: Default::default(),\n      git_account: Default::default(),\n      path: Default::default(),\n      on_clone: Default::default(),\n      on_pull: Default::default(),\n      links: Default::default(),\n      environment: Default::default(),\n      env_file_path: default_env_file_path(),\n      skip_secret_interp: Default::default(),\n      webhook_enabled: default_webhook_enabled(),\n      webhook_secret: Default::default(),\n    }\n  }\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]\npub struct RepoActionState {\n  /// Whether Repo currently cloning on the attached Server\n  pub cloning: bool,\n  /// Whether Repo currently pulling on the attached Server\n  pub pulling: bool,\n  /// Whether Repo currently building using the attached Builder.\n  pub building: bool,\n  /// Whether Repo currently renaming.\n  pub renaming: bool,\n}\n\n#[typeshare]\npub type RepoQuery = ResourceQuery<RepoQuerySpecifics>;\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,\n)]\npub struct RepoQuerySpecifics {\n  /// Filter repos by their repo.\n  pub repos: Vec<String>,\n}\n\nimpl super::resource::AddFilters for RepoQuerySpecifics {\n  fn add_filters(&self, filters: &mut Document) {\n    if !self.repos.is_empty() {\n      filters.insert(\"config.repo\", doc! { \"$in\": &self.repos });\n    }\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/resource.rs",
    "content": "use bson::{Document, doc};\nuse clap::ValueEnum;\nuse derive_builder::Builder;\nuse derive_default_builder::DefaultBuilder;\nuse serde::{Deserialize, Serialize};\nuse strum::Display;\nuse typeshare::typeshare;\n\nuse crate::{\n  deserializers::string_list_deserializer,\n  entities::{I64, MongoId},\n};\n\nuse super::{\n  ResourceTargetVariant, permission::PermissionLevelAndSpecifics,\n};\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize, Builder)]\npub struct Resource<Config: Default, Info: Default = ()> {\n  /// The Mongo ID of the resource.\n  /// This field is de/serialized from/to JSON as\n  /// `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Resource<T>) }`\n  #[serde(\n    default,\n    rename = \"_id\",\n    skip_serializing_if = \"String::is_empty\",\n    with = \"bson::serde_helpers::hex_string_as_object_id\"\n  )]\n  #[builder(setter(skip))]\n  pub id: MongoId,\n\n  /// The resource name.\n  /// This is guaranteed unique among others of the same resource type.\n  pub name: String,\n\n  /// A description for the resource\n  #[serde(default)]\n  #[builder(default)]\n  pub description: String,\n\n  /// Mark resource as a template\n  #[serde(default)]\n  #[builder(default)]\n  pub template: bool,\n\n  /// Tag Ids\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[builder(default)]\n  pub tags: Vec<String>,\n\n  /// Resource-specific information (not user configurable).\n  #[serde(default)]\n  #[builder(setter(skip))]\n  pub info: Info,\n\n  /// Resource-specific configuration.\n  #[serde(default)]\n  #[builder(default)]\n  pub config: Config,\n\n  /// Set a base permission level that all users will have on the\n  /// resource.\n  #[serde(default)]\n  #[builder(default)]\n  pub base_permission: PermissionLevelAndSpecifics,\n\n  /// When description last updated\n  #[serde(default)]\n  #[builder(setter(skip))]\n  pub updated_at: I64,\n}\n\nimpl<C: Default, I: Default> Default for Resource<C, I> {\n  fn default() -> Self {\n    Self {\n      id: String::new(),\n      name: String::from(\"temp-resource\"),\n      description: String::new(),\n      template: Default::default(),\n      tags: Vec::new(),\n      info: I::default(),\n      config: C::default(),\n      base_permission: Default::default(),\n      updated_at: 0,\n    }\n  }\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ResourceListItem<Info> {\n  /// The resource id\n  pub id: String,\n  /// The resource type, ie `Server` or `Deployment`\n  #[serde(rename = \"type\")]\n  pub resource_type: ResourceTargetVariant,\n  /// The resource name\n  pub name: String,\n  /// Whether resource is a template\n  pub template: bool,\n  /// Tag Ids\n  pub tags: Vec<String>,\n  /// Resource specific info\n  pub info: Info,\n}\n\n/// Passing empty Vec is the same as not filtering by that field\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,\n)]\npub struct ResourceQuery<T: Default> {\n  #[serde(default)]\n  pub names: Vec<String>,\n  #[serde(default)]\n  pub templates: TemplatesQueryBehavior,\n  /// Pass Vec of tag ids or tag names\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  pub tags: Vec<String>,\n  /// 'All' or 'Any'\n  #[serde(default)]\n  pub tag_behavior: TagQueryBehavior,\n  #[serde(default)]\n  pub specific: T,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  Default,\n  Serialize,\n  Deserialize,\n  ValueEnum,\n  Display,\n)]\n// Only strum serializes lowercase for clap compat.\n#[strum(serialize_all = \"lowercase\")]\npub enum TemplatesQueryBehavior {\n  /// Include templates in results. Default.\n  #[default]\n  Include,\n  /// Exclude templates from results.\n  Exclude,\n  /// Results *only* includes templates.\n  Only,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]\npub enum TagQueryBehavior {\n  /// Returns resources which have strictly all the tags\n  #[default]\n  All,\n  /// Returns resources which have one or more of the tags\n  Any,\n}\n\npub trait AddFilters {\n  fn add_filters(&self, _filters: &mut Document) {}\n}\n\nimpl AddFilters for () {}\n\nimpl<T: AddFilters + Default> AddFilters for ResourceQuery<T> {\n  fn add_filters(&self, filters: &mut Document) {\n    if !self.names.is_empty() {\n      filters.insert(\"name\", doc! { \"$in\": &self.names });\n    }\n    match self.templates {\n      TemplatesQueryBehavior::Exclude => {\n        filters.insert(\"template\", doc! { \"$ne\": true });\n      }\n      TemplatesQueryBehavior::Only => {\n        filters.insert(\"template\", true);\n      }\n      TemplatesQueryBehavior::Include => {\n        // No query on template field necessary\n      }\n    };\n    if !self.tags.is_empty() {\n      match self.tag_behavior {\n        TagQueryBehavior::All => {\n          filters.insert(\"tags\", doc! { \"$all\": &self.tags });\n        }\n        TagQueryBehavior::Any => {\n          let ors = self\n            .tags\n            .iter()\n            .map(|tag| doc! { \"tags\": tag })\n            .collect::<Vec<_>>();\n          filters.insert(\"$or\", ors);\n        }\n      }\n    }\n    self.specific.add_filters(filters);\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/schedule.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{I64, ResourceTarget, ScheduleFormat};\n\n/// A scheduled Action / Procedure run.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct Schedule {\n  /// Procedure or Alerter\n  pub target: ResourceTarget,\n  /// Readable name of the target resource\n  pub name: String,\n  /// The format of the schedule expression\n  pub schedule_format: ScheduleFormat,\n  /// The schedule for the run\n  pub schedule: String,\n  /// Whether the scheduled run is enabled\n  pub enabled: bool,\n  /// Custom schedule timezone if it exists\n  pub schedule_timezone: String,\n  /// Last run timestamp in ms.\n  pub last_run_at: Option<I64>,\n  /// Next scheduled run time in unix ms.\n  pub next_scheduled_run: Option<I64>,\n  /// If there is an error parsing schedule expression,\n  /// it will be given here.\n  pub schedule_error: Option<String>,\n  /// Resource tags.\n  pub tags: Vec<String>,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/server.rs",
    "content": "use std::{collections::HashMap, path::PathBuf};\n\nuse derive_builder::Builder;\nuse partial_derive2::Partial;\nuse serde::{Deserialize, Serialize};\nuse strum::Display;\nuse typeshare::typeshare;\n\nuse crate::{\n  deserializers::{\n    option_string_list_deserializer, string_list_deserializer,\n  },\n  entities::MaintenanceWindow,\n};\n\nuse super::{\n  I64,\n  alert::SeverityLevel,\n  resource::{AddFilters, Resource, ResourceListItem, ResourceQuery},\n};\n\n#[typeshare]\npub type Server = Resource<ServerConfig, ()>;\n\n#[typeshare]\npub type ServerListItem = ResourceListItem<ServerListItemInfo>;\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ServerListItemInfo {\n  /// The server's state.\n  pub state: ServerState,\n  /// Region of the server.\n  pub region: String,\n  /// Address of the server.\n  pub address: String,\n  /// External address of the server (reachable by users).\n  /// Used with links.\n  #[serde(default)] // API backward compat\n  pub external_address: String,\n  /// The Komodo Periphery version of the server.\n  pub version: String,\n  /// Whether server is configured to send unreachable alerts.\n  pub send_unreachable_alerts: bool,\n  /// Whether server is configured to send cpu alerts.\n  pub send_cpu_alerts: bool,\n  /// Whether server is configured to send mem alerts.\n  pub send_mem_alerts: bool,\n  /// Whether server is configured to send disk alerts.\n  pub send_disk_alerts: bool,\n  /// Whether server is configured to send version mismatch alerts.\n  pub send_version_mismatch_alerts: bool,\n  /// Whether terminals are disabled for this Server.\n  pub terminals_disabled: bool,\n  /// Whether container exec is disabled for this Server.\n  pub container_exec_disabled: bool,\n}\n\n#[typeshare(serialized_as = \"Partial<ServerConfig>\")]\npub type _PartialServerConfig = PartialServerConfig;\n\n/// Server configuration.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[partial(skip_serializing_none, from, diff)]\npub struct ServerConfig {\n  /// The http address of the periphery client.\n  /// Default: http://localhost:8120\n  #[serde(default = \"default_address\")]\n  #[builder(default = \"default_address()\")]\n  #[partial_default(default_address())]\n  pub address: String,\n\n  /// The address to use with links for containers on the server.\n  /// If empty, will use the 'address' for links.\n  #[serde(default)]\n  #[builder(default)]\n  pub external_address: String,\n\n  /// An optional region label\n  #[serde(default)]\n  #[builder(default)]\n  pub region: String,\n\n  /// Whether a server is enabled.\n  /// If a server is disabled,\n  /// you won't be able to perform any actions on it or see deployment's status.\n  /// Default: false\n  #[serde(default = \"default_enabled\")]\n  #[builder(default = \"default_enabled()\")]\n  #[partial_default(default_enabled())]\n  pub enabled: bool,\n\n  /// The timeout used to reach the server in seconds.\n  /// default: 2\n  #[serde(default = \"default_timeout_seconds\")]\n  #[builder(default = \"default_timeout_seconds()\")]\n  #[partial_default(default_timeout_seconds())]\n  pub timeout_seconds: I64,\n\n  /// An optional override passkey to use\n  /// to authenticate with periphery agent.\n  /// If this is empty, will use passkey in core config.\n  #[serde(default)]\n  #[builder(default)]\n  pub passkey: String,\n\n  /// Sometimes the system stats reports a mount path that is not desired.\n  /// Use this field to filter it out from the report.\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub ignore_mounts: Vec<String>,\n\n  /// Whether to monitor any server stats beyond passing health check.\n  /// default: true\n  #[serde(default = \"default_stats_monitoring\")]\n  #[builder(default = \"default_stats_monitoring()\")]\n  #[partial_default(default_stats_monitoring())]\n  pub stats_monitoring: bool,\n\n  /// Whether to trigger 'docker image prune -a -f' every 24 hours.\n  /// default: true\n  #[serde(default = \"default_auto_prune\")]\n  #[builder(default = \"default_auto_prune()\")]\n  #[partial_default(default_auto_prune())]\n  pub auto_prune: bool,\n\n  /// Configure quick links that are displayed in the resource header\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub links: Vec<String>,\n\n  /// Whether to send alerts about the servers reachability\n  #[serde(default = \"default_send_alerts\")]\n  #[builder(default = \"default_send_alerts()\")]\n  #[partial_default(default_send_alerts())]\n  pub send_unreachable_alerts: bool,\n\n  /// Whether to send alerts about the servers CPU status\n  #[serde(default = \"default_send_alerts\")]\n  #[builder(default = \"default_send_alerts()\")]\n  #[partial_default(default_send_alerts())]\n  pub send_cpu_alerts: bool,\n\n  /// Whether to send alerts about the servers MEM status\n  #[serde(default = \"default_send_alerts\")]\n  #[builder(default = \"default_send_alerts()\")]\n  #[partial_default(default_send_alerts())]\n  pub send_mem_alerts: bool,\n\n  /// Whether to send alerts about the servers DISK status\n  #[serde(default = \"default_send_alerts\")]\n  #[builder(default = \"default_send_alerts()\")]\n  #[partial_default(default_send_alerts())]\n  pub send_disk_alerts: bool,\n\n  /// Whether to send alerts about the servers version mismatch with core\n  #[serde(default = \"default_send_alerts\")]\n  #[builder(default = \"default_send_alerts()\")]\n  #[partial_default(default_send_alerts())]\n  pub send_version_mismatch_alerts: bool,\n\n  /// The percentage threshhold which triggers WARNING state for CPU.\n  #[serde(default = \"default_cpu_warning\")]\n  #[builder(default = \"default_cpu_warning()\")]\n  #[partial_default(default_cpu_warning())]\n  pub cpu_warning: f32,\n\n  /// The percentage threshhold which triggers CRITICAL state for CPU.\n  #[serde(default = \"default_cpu_critical\")]\n  #[builder(default = \"default_cpu_critical()\")]\n  #[partial_default(default_cpu_critical())]\n  pub cpu_critical: f32,\n\n  /// The percentage threshhold which triggers WARNING state for MEM.\n  #[serde(default = \"default_mem_warning\")]\n  #[builder(default = \"default_mem_warning()\")]\n  #[partial_default(default_mem_warning())]\n  pub mem_warning: f64,\n\n  /// The percentage threshhold which triggers CRITICAL state for MEM.\n  #[serde(default = \"default_mem_critical\")]\n  #[builder(default = \"default_mem_critical()\")]\n  #[partial_default(default_mem_critical())]\n  pub mem_critical: f64,\n\n  /// The percentage threshhold which triggers WARNING state for DISK.\n  #[serde(default = \"default_disk_warning\")]\n  #[builder(default = \"default_disk_warning()\")]\n  #[partial_default(default_disk_warning())]\n  pub disk_warning: f64,\n\n  /// The percentage threshhold which triggers CRITICAL state for DISK.\n  #[serde(default = \"default_disk_critical\")]\n  #[builder(default = \"default_disk_critical()\")]\n  #[partial_default(default_disk_critical())]\n  pub disk_critical: f64,\n\n  /// Scheduled maintenance windows during which alerts will be suppressed.\n  #[serde(default)]\n  #[builder(default)]\n  pub maintenance_windows: Vec<MaintenanceWindow>,\n}\n\nimpl ServerConfig {\n  pub fn builder() -> ServerConfigBuilder {\n    ServerConfigBuilder::default()\n  }\n}\n\nfn default_address() -> String {\n  String::from(\"https://periphery:8120\")\n}\n\nfn default_enabled() -> bool {\n  false\n}\n\nfn default_timeout_seconds() -> i64 {\n  3\n}\n\nfn default_stats_monitoring() -> bool {\n  true\n}\n\nfn default_auto_prune() -> bool {\n  true\n}\n\nfn default_send_alerts() -> bool {\n  true\n}\n\nfn default_cpu_warning() -> f32 {\n  90.0\n}\n\nfn default_cpu_critical() -> f32 {\n  99.0\n}\n\nfn default_mem_warning() -> f64 {\n  75.0\n}\n\nfn default_mem_critical() -> f64 {\n  95.0\n}\n\nfn default_disk_warning() -> f64 {\n  75.0\n}\n\nfn default_disk_critical() -> f64 {\n  95.0\n}\n\nimpl Default for ServerConfig {\n  fn default() -> Self {\n    Self {\n      address: default_address(),\n      external_address: Default::default(),\n      enabled: default_enabled(),\n      timeout_seconds: default_timeout_seconds(),\n      ignore_mounts: Default::default(),\n      stats_monitoring: default_stats_monitoring(),\n      auto_prune: default_auto_prune(),\n      links: Default::default(),\n      send_unreachable_alerts: default_send_alerts(),\n      send_cpu_alerts: default_send_alerts(),\n      send_mem_alerts: default_send_alerts(),\n      send_disk_alerts: default_send_alerts(),\n      send_version_mismatch_alerts: default_send_alerts(),\n      region: Default::default(),\n      passkey: Default::default(),\n      cpu_warning: default_cpu_warning(),\n      cpu_critical: default_cpu_critical(),\n      mem_warning: default_mem_warning(),\n      mem_critical: default_mem_critical(),\n      disk_warning: default_disk_warning(),\n      disk_critical: default_disk_critical(),\n      maintenance_windows: Default::default(),\n    }\n  }\n}\n\n/// The health of a part of the server.\n#[typeshare]\n#[derive(Serialize, Deserialize, Default, Debug, Clone)]\npub struct ServerHealthState {\n  pub level: SeverityLevel,\n  /// Whether the health is good enough to close an open alert.\n  pub should_close_alert: bool,\n}\n\n/// Summary of the health of the server.\n#[typeshare]\n#[derive(Serialize, Deserialize, Default, Debug, Clone)]\npub struct ServerHealth {\n  pub cpu: ServerHealthState,\n  pub mem: ServerHealthState,\n  pub disks: HashMap<PathBuf, ServerHealthState>,\n}\n\n/// Info about an active terminal on a server.\n/// Retrieve with [ListTerminals][crate::api::read::server::ListTerminals].\n#[typeshare]\n#[derive(Serialize, Deserialize, Default, Debug, Clone)]\npub struct TerminalInfo {\n  /// The name of the terminal.\n  pub name: String,\n  /// The root program / args of the pty\n  pub command: String,\n  /// The size of the terminal history in memory.\n  pub stored_size_kb: f64,\n}\n\n/// Current pending actions on the server.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]\npub struct ServerActionState {\n  /// Server currently pruning networks\n  pub pruning_networks: bool,\n  /// Server currently pruning containers\n  pub pruning_containers: bool,\n  /// Server currently pruning images\n  pub pruning_images: bool,\n  /// Server currently pruning volumes\n  pub pruning_volumes: bool,\n  /// Server currently pruning docker builders\n  pub pruning_builders: bool,\n  /// Server currently pruning builx cache\n  pub pruning_buildx: bool,\n  /// Server currently pruning system\n  pub pruning_system: bool,\n  /// Server currently starting containers.\n  pub starting_containers: bool,\n  /// Server currently restarting containers.\n  pub restarting_containers: bool,\n  /// Server currently pausing containers.\n  pub pausing_containers: bool,\n  /// Server currently unpausing containers.\n  pub unpausing_containers: bool,\n  /// Server currently stopping containers.\n  pub stopping_containers: bool,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  Hash,\n  PartialOrd,\n  Ord,\n  Default,\n  Display,\n  Serialize,\n  Deserialize,\n)]\n#[strum(serialize_all = \"kebab-case\")]\npub enum ServerState {\n  /// Server health check passing.\n  Ok,\n  /// Server is unreachable.\n  #[default]\n  NotOk,\n  /// Server is disabled.\n  Disabled,\n}\n\n/// Server-specific query\n#[typeshare]\npub type ServerQuery = ResourceQuery<ServerQuerySpecifics>;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct ServerQuerySpecifics {}\n\nimpl AddFilters for ServerQuerySpecifics {}\n"
  },
  {
    "path": "client/core/rs/src/entities/stack.rs",
    "content": "use std::{collections::HashMap, sync::OnceLock};\n\nuse anyhow::Context;\nuse bson::{Document, doc};\nuse derive_builder::Builder;\nuse derive_default_builder::DefaultBuilder;\nuse indexmap::IndexSet;\nuse partial_derive2::Partial;\nuse serde::{\n  Deserialize, Serialize,\n  de::{IntoDeserializer, Visitor, value::MapAccessDeserializer},\n};\nuse strum::Display;\nuse typeshare::typeshare;\n\nuse crate::{\n  deserializers::{\n    env_vars_deserializer, file_contents_deserializer,\n    option_env_vars_deserializer, option_file_contents_deserializer,\n    option_maybe_string_i64_deserializer,\n    option_string_list_deserializer, string_list_deserializer,\n  },\n  entities::{EnvironmentVar, environment_vars_from_str},\n};\n\nuse super::{\n  FileContents, SystemCommand,\n  docker::container::ContainerListItem,\n  resource::{Resource, ResourceListItem, ResourceQuery},\n};\n\n#[typeshare]\npub type Stack = Resource<StackConfig, StackInfo>;\n\nimpl Stack {\n  /// If fresh is passed, it will bypass the deployed project name.\n  /// and get the most up to date one from just project_name field falling back to stack name.\n  pub fn project_name(&self, fresh: bool) -> String {\n    if !fresh\n      && let Some(project_name) = &self.info.deployed_project_name\n    {\n      return project_name.clone();\n    }\n    if self.config.project_name.is_empty() {\n      self.name.clone()\n    } else {\n      self.config.project_name.clone()\n    }\n  }\n\n  pub fn compose_file_paths(&self) -> &[String] {\n    if self.config.file_paths.is_empty() {\n      default_stack_file_paths()\n    } else {\n      &self.config.file_paths\n    }\n  }\n\n  pub fn is_compose_file(&self, path: &str) -> bool {\n    for compose_path in self.compose_file_paths() {\n      if path.ends_with(compose_path) {\n        return true;\n      }\n    }\n    false\n  }\n\n  pub fn all_file_paths(&self) -> Vec<String> {\n    let mut res = self\n      .compose_file_paths()\n      .iter()\n      .cloned()\n      // Makes sure to dedup them, while maintaining ordering\n      .collect::<IndexSet<_>>();\n    res.extend(self.config.additional_env_files.clone());\n    res.extend(\n      self.config.config_files.iter().map(|f| f.path.clone()),\n    );\n    res.into_iter().collect()\n  }\n\n  pub fn all_file_dependencies(&self) -> Vec<StackFileDependency> {\n    let mut res = self\n      .compose_file_paths()\n      .iter()\n      .cloned()\n      .map(StackFileDependency::full_redeploy)\n      // Makes sure to dedup them, while maintaining ordering\n      .collect::<IndexSet<_>>();\n    res.extend(\n      self\n        .config\n        .additional_env_files\n        .iter()\n        .cloned()\n        .map(StackFileDependency::full_redeploy),\n    );\n    res.extend(self.config.config_files.clone());\n    res.into_iter().collect()\n  }\n}\n\nfn default_stack_file_paths() -> &'static [String] {\n  static DEFAULT_FILE_PATHS: OnceLock<Vec<String>> = OnceLock::new();\n  DEFAULT_FILE_PATHS\n    .get_or_init(|| vec![String::from(\"compose.yaml\")])\n}\n\n#[typeshare]\npub type StackListItem = ResourceListItem<StackListItemInfo>;\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct StackListItemInfo {\n  /// The server that stack is deployed on.\n  pub server_id: String,\n  /// Whether stack is using files on host mode\n  pub files_on_host: bool,\n  /// Whether stack has file contents defined.\n  pub file_contents: bool,\n  /// Linked repo, if one is attached.\n  pub linked_repo: String,\n  /// The git provider domain\n  pub git_provider: String,\n  /// The configured repo\n  pub repo: String,\n  /// The configured branch\n  pub branch: String,\n  /// Full link to the repo.\n  pub repo_link: String,\n  /// The stack state\n  pub state: StackState,\n  /// A string given by docker conveying the status of the stack.\n  pub status: Option<String>,\n  /// The services that are part of the stack.\n  /// If deployed, will be `deployed_services`.\n  /// Otherwise, its `latest_services`\n  pub services: Vec<StackServiceWithUpdate>,\n  /// Whether the compose project is missing on the host.\n  /// Ie, it does not show up in `docker compose ls`.\n  /// If true, and the stack is not Down, this is an unhealthy state.\n  pub project_missing: bool,\n  /// If any compose files are missing in the repo, the path will be here.\n  /// If there are paths here, this is an unhealthy state, and deploying will fail.\n  pub missing_files: Vec<String>,\n  /// Deployed short commit hash, or null. Only for repo based stacks.\n  pub deployed_hash: Option<String>,\n  /// Latest short commit hash, or null. Only for repo based stacks\n  pub latest_hash: Option<String>,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct StackServiceWithUpdate {\n  pub service: String,\n  /// The service's image\n  pub image: String,\n  /// Whether there is a newer image available for this service\n  pub update_available: bool,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  Default,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n  Serialize,\n  Deserialize,\n  Display,\n)]\n// Do this one snake_case in line with DeploymentState.\n// Also in line with docker terminology.\n#[serde(rename_all = \"snake_case\")]\n#[strum(serialize_all = \"snake_case\")]\npub enum StackState {\n  /// The stack is currently re/deploying\n  Deploying,\n  /// All containers are running.\n  Running,\n  /// All containers are paused\n  Paused,\n  /// All contianers are stopped\n  Stopped,\n  /// All containers are created\n  Created,\n  /// All containers are restarting\n  Restarting,\n  /// All containers are dead\n  Dead,\n  /// All containers are removing\n  Removing,\n  /// The containers are in a mix of states\n  Unhealthy,\n  /// The stack is not deployed\n  Down,\n  /// Server not reachable for status\n  #[default]\n  Unknown,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct StackInfo {\n  /// If any of the expected compose / additional files are missing in the repo,\n  /// they will be stored here.\n  #[serde(default)]\n  pub missing_files: Vec<String>,\n\n  /// The deployed project name.\n  /// This is updated whenever Komodo successfully deploys the stack.\n  /// If it is present, Komodo will use it for actions over other options,\n  /// to ensure control is maintained after changing the project name (there is no rename compose project api).\n  pub deployed_project_name: Option<String>,\n\n  /// Deployed short commit hash, or null. Only for repo based stacks.\n  pub deployed_hash: Option<String>,\n  /// Deployed commit message, or null. Only for repo based stacks\n  pub deployed_message: Option<String>,\n  /// The deployed compose / additional file contents.\n  /// This is updated whenever Komodo successfully deploys the stack.\n  pub deployed_contents: Option<Vec<FileContents>>,\n  /// The deployed service names.\n  /// This is updated whenever it is empty, or deployed contents is updated.\n  pub deployed_services: Option<Vec<StackServiceNames>>,\n  /// The output of `docker compose config`.\n  /// This is updated whenever Komodo successfully deploys the stack.\n  pub deployed_config: Option<String>,\n  /// The latest service names.\n  /// This is updated whenever the stack cache refreshes, using the latest file contents (either db defined or remote).\n  #[serde(default)]\n  pub latest_services: Vec<StackServiceNames>,\n\n  /// The remote compose / additional file contents, whether on host or in repo.\n  /// This is updated whenever Komodo refreshes the stack cache.\n  /// It will be empty if the file is defined directly in the stack config.\n  pub remote_contents: Option<Vec<StackRemoteFileContents>>,\n  /// If there was an error in getting the remote contents, it will be here.\n  pub remote_errors: Option<Vec<FileContents>>,\n\n  /// Latest commit hash, or null\n  pub latest_hash: Option<String>,\n  /// Latest commit message, or null\n  pub latest_message: Option<String>,\n}\n\n#[typeshare(serialized_as = \"Partial<StackConfig>\")]\npub type _PartialStackConfig = PartialStackConfig;\n\n/// The compose file configuration.\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize, Builder, Partial)]\n#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)]\n#[partial(skip_serializing_none, from, diff)]\npub struct StackConfig {\n  /// The server to deploy the stack on.\n  #[serde(default, alias = \"server\")]\n  #[partial_attr(serde(alias = \"server\"))]\n  #[builder(default)]\n  pub server_id: String,\n\n  /// Configure quick links that are displayed in the resource header\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub links: Vec<String>,\n\n  /// Optionally specify a custom project name for the stack.\n  /// If this is empty string, it will default to the stack name.\n  /// Used with `docker compose -p {project_name}`.\n  ///\n  /// Note. Can be used to import pre-existing stacks.\n  #[serde(default)]\n  #[builder(default)]\n  pub project_name: String,\n\n  /// Whether to automatically `compose pull` before redeploying stack.\n  /// Ensured latest images are deployed.\n  /// Will fail if the compose file specifies a locally build image.\n  #[serde(default = \"default_auto_pull\")]\n  #[builder(default = \"default_auto_pull()\")]\n  #[partial_default(default_auto_pull())]\n  pub auto_pull: bool,\n\n  /// Whether to `docker compose build` before `compose down` / `compose up`.\n  /// Combine with build_extra_args for custom behaviors.\n  #[serde(default)]\n  #[builder(default)]\n  pub run_build: bool,\n\n  /// Whether to poll for any updates to the images.\n  #[serde(default)]\n  #[builder(default)]\n  pub poll_for_updates: bool,\n\n  /// Whether to automatically redeploy when\n  /// newer images are found. Will implicitly\n  /// enable `poll_for_updates`, you don't need to\n  /// enable both.\n  #[serde(default)]\n  #[builder(default)]\n  pub auto_update: bool,\n\n  /// If auto update is enabled, Komodo will\n  /// by default only update the specific services\n  /// with image updates. If this parameter is set to true,\n  /// Komodo will redeploy the whole Stack (all services).\n  #[serde(default)]\n  #[builder(default)]\n  pub auto_update_all_services: bool,\n\n  /// Whether to run `docker compose down` before `compose up`.\n  #[serde(default)]\n  #[builder(default)]\n  pub destroy_before_deploy: bool,\n\n  /// Whether to skip secret interpolation into the stack environment variables.\n  #[serde(default)]\n  #[builder(default)]\n  pub skip_secret_interp: bool,\n\n  /// Choose a Komodo Repo (Resource) to source the compose files.\n  #[serde(default)]\n  #[builder(default)]\n  pub linked_repo: String,\n\n  /// The git provider domain. Default: github.com\n  #[serde(default = \"default_git_provider\")]\n  #[builder(default = \"default_git_provider()\")]\n  #[partial_default(default_git_provider())]\n  pub git_provider: String,\n\n  /// Whether to use https to clone the repo (versus http). Default: true\n  ///\n  /// Note. Komodo does not currently support cloning repos via ssh.\n  #[serde(default = \"default_git_https\")]\n  #[builder(default = \"default_git_https()\")]\n  #[partial_default(default_git_https())]\n  pub git_https: bool,\n\n  /// The git account used to access private repos.\n  /// Passing empty string can only clone public repos.\n  ///\n  /// Note. A token for the account must be available in the core config or the builder server's periphery config\n  /// for the configured git provider.\n  #[serde(default)]\n  #[builder(default)]\n  pub git_account: String,\n\n  /// The repo used as the source of the build.\n  /// {namespace}/{repo_name}\n  #[serde(default)]\n  #[builder(default)]\n  pub repo: String,\n\n  /// The branch of the repo.\n  #[serde(default = \"default_branch\")]\n  #[builder(default = \"default_branch()\")]\n  #[partial_default(default_branch())]\n  pub branch: String,\n\n  /// Optionally set a specific commit hash.\n  #[serde(default)]\n  #[builder(default)]\n  pub commit: String,\n\n  /// Optionally set a specific clone path\n  #[serde(default)]\n  #[builder(default)]\n  pub clone_path: String,\n\n  /// By default, the Stack will `git pull` the repo after it is first cloned.\n  /// If this option is enabled, the repo folder will be deleted and recloned instead.\n  #[serde(default)]\n  #[builder(default)]\n  pub reclone: bool,\n\n  /// Whether incoming webhooks actually trigger action.\n  #[serde(default = \"default_webhook_enabled\")]\n  #[builder(default = \"default_webhook_enabled()\")]\n  #[partial_default(default_webhook_enabled())]\n  pub webhook_enabled: bool,\n\n  /// Optionally provide an alternate webhook secret for this stack.\n  /// If its an empty string, use the default secret from the config.\n  #[serde(default)]\n  #[builder(default)]\n  pub webhook_secret: String,\n\n  /// By default, the Stack will `DeployStackIfChanged`.\n  /// If this option is enabled, will always run `DeployStack` without diffing.\n  #[serde(default)]\n  #[builder(default)]\n  pub webhook_force_deploy: bool,\n\n  /// If this is checked, the stack will source the files on the host.\n  /// Use `run_directory` and `file_paths` to specify the path on the host.\n  /// This is useful for those who wish to setup their files on the host,\n  /// rather than defining the contents in UI or in a git repo.\n  #[serde(default)]\n  #[builder(default)]\n  pub files_on_host: bool,\n\n  /// Directory to change to (`cd`) before running `docker compose up -d`.\n  #[serde(default)]\n  #[builder(default)]\n  pub run_directory: String,\n\n  /// Add paths to compose files, relative to the run path.\n  /// If this is empty, will use file `compose.yaml`.\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub file_paths: Vec<String>,\n\n  /// The name of the written environment file before `docker compose up`.\n  /// Relative to the run directory root.\n  /// Default: .env\n  #[serde(default = \"default_env_file_path\")]\n  #[builder(default = \"default_env_file_path()\")]\n  #[partial_default(default_env_file_path())]\n  pub env_file_path: String,\n\n  /// Add additional env files to attach with `--env-file`.\n  /// Relative to the run directory root.\n  ///\n  /// Note. It is already included as an `additional_file`.\n  /// Don't add it again there.\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub additional_env_files: Vec<String>,\n\n  /// Add additional config files either in repo or on host to track.\n  /// Can add any files associated with the stack to enable editing them in the UI.\n  /// Doing so will also include diffing these when deciding to deploy in `DeployStackIfChanged`.\n  /// Relative to the run directory.\n  ///\n  /// Note. If the config file is .env and should be included in compose command\n  /// using `--env-file`, add it to `additional_env_files` instead.\n  #[serde(default)]\n  #[partial_attr(serde(default))]\n  #[builder(default)]\n  pub config_files: Vec<StackFileDependency>,\n\n  /// Whether to send StackStateChange alerts for this stack.\n  #[serde(default = \"default_send_alerts\")]\n  #[builder(default = \"default_send_alerts()\")]\n  #[partial_default(default_send_alerts())]\n  pub send_alerts: bool,\n\n  /// Used with `registry_account` to login to a registry before docker compose up.\n  #[serde(default)]\n  #[builder(default)]\n  pub registry_provider: String,\n\n  /// Used with `registry_provider` to login to a registry before docker compose up.\n  #[serde(default)]\n  #[builder(default)]\n  pub registry_account: String,\n\n  /// The optional command to run before the Stack is deployed.\n  #[serde(default)]\n  #[builder(default)]\n  pub pre_deploy: SystemCommand,\n\n  /// The optional command to run after the Stack is deployed.\n  #[serde(default)]\n  #[builder(default)]\n  pub post_deploy: SystemCommand,\n\n  /// The extra arguments to pass after `docker compose up -d`.\n  /// If empty, no extra arguments will be passed.\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub extra_args: Vec<String>,\n\n  /// The extra arguments to pass after `docker compose build`.\n  /// If empty, no extra build arguments will be passed.\n  /// Only used if `run_build: true`\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub build_extra_args: Vec<String>,\n\n  /// Ignore certain services declared in the compose file when checking\n  /// the stack status. For example, an init service might be exited, but the\n  /// stack should be healthy. This init service should be in `ignore_services`\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub ignore_services: Vec<String>,\n\n  /// The contents of the file directly, for management in the UI.\n  /// If this is empty, it will fall back to checking git config for\n  /// repo based compose file.\n  /// Supports variable / secret interpolation.\n  #[serde(default, deserialize_with = \"file_contents_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_file_contents_deserializer\"\n  ))]\n  #[builder(default)]\n  pub file_contents: String,\n\n  /// The environment variables passed to the compose file.\n  /// They will be written to path defined in env_file_path,\n  /// which is given relative to the run directory.\n  ///\n  /// If it is empty, no file will be written.\n  #[serde(default, deserialize_with = \"env_vars_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_env_vars_deserializer\"\n  ))]\n  #[builder(default)]\n  pub environment: String,\n}\n\nimpl StackConfig {\n  pub fn builder() -> StackConfigBuilder {\n    StackConfigBuilder::default()\n  }\n\n  pub fn env_vars(&self) -> anyhow::Result<Vec<EnvironmentVar>> {\n    environment_vars_from_str(&self.environment)\n      .context(\"Invalid environment\")\n  }\n}\n\nfn default_env_file_path() -> String {\n  String::from(\".env\")\n}\n\nfn default_auto_pull() -> bool {\n  true\n}\n\nfn default_git_provider() -> String {\n  String::from(\"github.com\")\n}\n\nfn default_git_https() -> bool {\n  true\n}\n\nfn default_branch() -> String {\n  String::from(\"main\")\n}\n\nfn default_webhook_enabled() -> bool {\n  true\n}\n\nfn default_send_alerts() -> bool {\n  true\n}\n\nimpl Default for StackConfig {\n  fn default() -> Self {\n    Self {\n      server_id: Default::default(),\n      project_name: Default::default(),\n      run_directory: Default::default(),\n      file_paths: Default::default(),\n      files_on_host: Default::default(),\n      registry_provider: Default::default(),\n      registry_account: Default::default(),\n      file_contents: Default::default(),\n      auto_pull: default_auto_pull(),\n      poll_for_updates: Default::default(),\n      auto_update: Default::default(),\n      auto_update_all_services: Default::default(),\n      ignore_services: Default::default(),\n      pre_deploy: Default::default(),\n      post_deploy: Default::default(),\n      extra_args: Default::default(),\n      environment: Default::default(),\n      env_file_path: default_env_file_path(),\n      additional_env_files: Default::default(),\n      config_files: Default::default(),\n      run_build: Default::default(),\n      destroy_before_deploy: Default::default(),\n      build_extra_args: Default::default(),\n      skip_secret_interp: Default::default(),\n      linked_repo: Default::default(),\n      git_provider: default_git_provider(),\n      git_https: default_git_https(),\n      repo: Default::default(),\n      branch: default_branch(),\n      commit: Default::default(),\n      clone_path: Default::default(),\n      reclone: Default::default(),\n      git_account: Default::default(),\n      webhook_enabled: default_webhook_enabled(),\n      webhook_secret: Default::default(),\n      webhook_force_deploy: Default::default(),\n      send_alerts: default_send_alerts(),\n      links: Default::default(),\n    }\n  }\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct ComposeProject {\n  /// The compose project name.\n  pub name: String,\n  /// The status of the project, as returned by docker.\n  pub status: Option<String>,\n  /// The compose files included in the project.\n  pub compose_files: Vec<String>,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct StackServiceNames {\n  /// The name of the service\n  pub service_name: String,\n  /// Will either be the declared container_name in the compose file,\n  /// or a pattern to match auto named containers.\n  ///\n  /// Auto named containers are composed of three parts:\n  ///\n  /// 1. The name of the compose project (top level name field of compose file).\n  ///    This defaults to the name of the parent folder of the compose file.\n  ///    Komodo will always set it to be the name of the stack, but imported stacks\n  ///    will have a different name.\n  /// 2. The service name\n  /// 3. The replica number\n  ///\n  /// Example: stacko-mongo-1.\n  ///\n  /// This stores only 1. and 2., ie stacko-mongo.\n  /// Containers will be matched via regex like `^container_name-?[0-9]*$``\n  pub container_name: String,\n  /// The services image.\n  #[serde(default)]\n  pub image: String,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct StackService {\n  /// The service name\n  pub service: String,\n  /// The service image\n  pub image: String,\n  /// The container\n  pub container: Option<ContainerListItem>,\n  /// Whether there is an update available for this services image.\n  pub update_available: bool,\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]\npub struct StackActionState {\n  pub pulling: bool,\n  pub deploying: bool,\n  pub starting: bool,\n  pub restarting: bool,\n  pub pausing: bool,\n  pub unpausing: bool,\n  pub stopping: bool,\n  pub destroying: bool,\n}\n\n#[typeshare]\npub type StackQuery = ResourceQuery<StackQuerySpecifics>;\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,\n)]\npub struct StackQuerySpecifics {\n  /// Query only for Stacks on these Servers.\n  /// If empty, does not filter by Server.\n  /// Only accepts Server id (not name).\n  #[serde(default)]\n  pub server_ids: Vec<String>,\n  /// Query only for Stacks with these linked repos.\n  /// Only accepts Repo id (not name).\n  #[serde(default)]\n  pub linked_repos: Vec<String>,\n  /// Filter syncs by their repo.\n  #[serde(default)]\n  pub repos: Vec<String>,\n  /// Query only for Stack with available image updates.\n  #[serde(default)]\n  pub update_available: bool,\n}\n\nimpl super::resource::AddFilters for StackQuerySpecifics {\n  fn add_filters(&self, filters: &mut Document) {\n    if !self.server_ids.is_empty() {\n      filters\n        .insert(\"config.server_id\", doc! { \"$in\": &self.server_ids });\n    }\n    if !self.linked_repos.is_empty() {\n      filters.insert(\n        \"config.linked_repo\",\n        doc! { \"$in\": &self.linked_repos },\n      );\n    }\n    if !self.repos.is_empty() {\n      filters.insert(\"config.repo\", doc! { \"$in\": &self.repos });\n    }\n  }\n}\n\n/// Keeping this minimal for now as its only needed to parse the service names / container names,\n/// and replica count. Not a typeshared type.\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct ComposeFile {\n  /// If not provided, will default to the parent folder holding the compose file.\n  pub name: Option<String>,\n  #[serde(default)]\n  pub services: HashMap<String, ComposeService>,\n}\n\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct ComposeService {\n  pub image: Option<String>,\n  pub container_name: Option<String>,\n  pub deploy: Option<ComposeServiceDeploy>,\n}\n\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct ComposeServiceDeploy {\n  #[serde(\n    default,\n    deserialize_with = \"option_maybe_string_i64_deserializer\"\n  )]\n  pub replicas: Option<i64>,\n}\n\n// PRE-1.19.1 BACKWARD COMPAT NOTE\n// This was split from general FileContents in 1.19.1,\n// and must maintain 2 way de/ser backward compatibility\n// with the mentioned struct.\n/// Same as [FileContents] with some extra\n/// info specific to Stacks.\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct StackRemoteFileContents {\n  /// The path to the file\n  pub path: String,\n  /// The contents of the file\n  pub contents: String,\n  /// The services depending on this file,\n  /// or empty for global requirement (eg all compose files and env files).\n  #[serde(default)]\n  pub services: Vec<String>,\n  /// Whether diff requires Redeploy / Restart / None\n  #[serde(default)]\n  pub requires: StackFileRequires,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  PartialEq,\n  Eq,\n  Hash,\n  Default,\n  Serialize,\n  Deserialize,\n)]\npub enum StackFileRequires {\n  /// Diff requires service redeploy.\n  #[serde(alias = \"redeploy\")]\n  Redeploy,\n  /// Diff requires service restart\n  #[serde(alias = \"restart\")]\n  Restart,\n  /// Diff requires no action. Default.\n  #[default]\n  #[serde(alias = \"none\")]\n  None,\n}\n\n/// Configure additional file dependencies of the Stack.\n#[typeshare]\n#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]\npub struct StackFileDependency {\n  /// Specify the file\n  pub path: String,\n  /// Specify specific service/s\n  #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n  pub services: Vec<String>,\n  /// Specify\n  #[serde(default, skip_serializing_if = \"is_none\")]\n  pub requires: StackFileRequires,\n}\n\nimpl StackFileDependency {\n  pub fn full_redeploy(path: String) -> StackFileDependency {\n    StackFileDependency {\n      path,\n      services: Vec::new(),\n      requires: StackFileRequires::Redeploy,\n    }\n  }\n}\n\nfn is_none(requires: &StackFileRequires) -> bool {\n  matches!(requires, StackFileRequires::None)\n}\n\n/// Used with custom de/serializer for [StackFileDependency]\n#[derive(Deserialize)]\nstruct __StackFileDependency {\n  path: String,\n  #[serde(\n    default,\n    alias = \"service\",\n    deserialize_with = \"string_list_deserializer\"\n  )]\n  services: Vec<String>,\n  #[serde(default, alias = \"req\")]\n  requires: StackFileRequires,\n}\n\nimpl<'de> Deserialize<'de> for StackFileDependency {\n  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n  where\n    D: serde::Deserializer<'de>,\n  {\n    struct StackFileDependencyVisitor;\n\n    impl<'de> Visitor<'de> for StackFileDependencyVisitor {\n      type Value = StackFileDependency;\n\n      fn expecting(\n        &self,\n        formatter: &mut std::fmt::Formatter,\n      ) -> std::fmt::Result {\n        write!(formatter, \"string or StackFileDependency (object)\")\n      }\n\n      fn visit_string<E>(self, path: String) -> Result<Self::Value, E>\n      where\n        E: serde::de::Error,\n      {\n        Ok(StackFileDependency {\n          path,\n          services: Vec::new(),\n          requires: StackFileRequires::None,\n        })\n      }\n\n      fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n      where\n        E: serde::de::Error,\n      {\n        Self::visit_string(self, v.to_string())\n      }\n\n      fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>\n      where\n        A: serde::de::MapAccess<'de>,\n      {\n        __StackFileDependency::deserialize(\n          MapAccessDeserializer::new(map).into_deserializer(),\n        )\n        .map(|v| StackFileDependency {\n          path: v.path,\n          services: v.services,\n          requires: v.requires,\n        })\n      }\n    }\n\n    deserializer.deserialize_any(StackFileDependencyVisitor)\n  }\n}\n\n// // This one is nice for TOML, but annoying to use on frontend\n// impl Serialize for StackFileDependency {\n//   fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n//   where\n//     S: serde::Serializer,\n//   {\n//     // Serialize to string in default case\n//     if is_redeploy(&self.requires) && self.services.is_empty() {\n//       return serializer.serialize_str(&self.path);\n//     }\n//     __StackFileDependency {\n//       path: self.path.clone(),\n//       services: self.services.clone(),\n//       requires: self.requires,\n//     }\n//     .serialize(serializer)\n//   }\n// }\n"
  },
  {
    "path": "client/core/rs/src/entities/stats.rs",
    "content": "use std::path::PathBuf;\n\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{I64, Timelength};\n\n/// System information of a server\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct SystemInformation {\n  /// The system name\n  pub name: Option<String>,\n  /// The system long os version\n  pub os: Option<String>,\n  /// System's kernel version\n  pub kernel: Option<String>,\n  /// Physical core count\n  pub core_count: Option<u32>,\n  /// System hostname based off DNS\n  pub host_name: Option<String>,\n  /// The CPU's brand\n  pub cpu_brand: String,\n  /// Whether terminals are disabled on this Periphery server\n  pub terminals_disabled: bool,\n  /// Whether container exec is disabled on this Periphery server\n  pub container_exec_disabled: bool,\n}\n\n/// System stats stored on the database.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\n#[cfg_attr(feature = \"mongo\", collection_name(Stats))]\npub struct SystemStatsRecord {\n  /// Unix timestamp in milliseconds\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub ts: I64,\n  /// Server id\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub sid: String,\n  // basic stats\n  /// Cpu usage percentage\n  pub cpu_perc: f32,\n  /// Load average (1m, 5m, 15m)\n  #[serde(default)]\n  pub load_average: SystemLoadAverage,\n  /// Memory used in GB\n  pub mem_used_gb: f64,\n  /// Total memory in GB\n  pub mem_total_gb: f64,\n  /// Disk used in GB\n  pub disk_used_gb: f64,\n  /// Total disk size in GB\n  pub disk_total_gb: f64,\n  /// Breakdown of individual disks, including their usage, total size, and mount point\n  pub disks: Vec<SingleDiskUsage>,\n  /// Total network ingress in bytes\n  #[serde(default)]\n  pub network_ingress_bytes: f64,\n  /// Total network egress in bytes\n  #[serde(default)]\n  pub network_egress_bytes: f64,\n  // /// Network usage by interface name (ingress, egress in bytes)\n  // #[serde(default)]\n  // pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)\n}\n\n/// Realtime system stats data.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Default, Clone)]\npub struct SystemStats {\n  /// Cpu usage percentage\n  pub cpu_perc: f32,\n  ///  Load average (1m, 5m, 15m)\n  #[serde(default)]\n  pub load_average: SystemLoadAverage,\n  /// [1.15.9+]\n  /// Free memory in GB.\n  /// This is really the 'Free' memory, not the 'Available' memory.\n  /// It may be different than mem_total_gb - mem_used_gb.\n  #[serde(default)]\n  pub mem_free_gb: f64,\n  /// Used memory in GB. 'Total' - 'Available' (not free) memory.\n  pub mem_used_gb: f64,\n  /// Total memory in GB\n  pub mem_total_gb: f64,\n  /// Breakdown of individual disks, ie their usages, sizes, and mount points\n  pub disks: Vec<SingleDiskUsage>,\n  /// Network ingress usage in MB\n  #[serde(default)]\n  pub network_ingress_bytes: f64,\n  /// Network egress usage in MB\n  #[serde(default)]\n  pub network_egress_bytes: f64,\n  // /// Network usage by interface name (ingress, egress in bytes)\n  // #[serde(default)]\n  // pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)\n  // metadata\n  /// The rate the system stats are being polled from the system\n  pub polling_rate: Timelength,\n  /// Unix timestamp in milliseconds when stats were last polled\n  pub refresh_ts: I64,\n  /// Unix timestamp in milliseconds when disk list was last refreshed\n  pub refresh_list_ts: I64,\n}\n\n/// Info for a single disk mounted on the system.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct SingleDiskUsage {\n  /// The mount point of the disk\n  pub mount: PathBuf,\n  /// Detected file system\n  pub file_system: String,\n  /// Used portion of the disk in GB\n  pub used_gb: f64,\n  /// Total size of the disk in GB\n  pub total_gb: f64,\n}\n\n/// Info for network interface usage.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct SingleNetworkInterfaceUsage {\n  /// The network interface name\n  pub name: String,\n  /// The ingress in bytes\n  pub ingress_bytes: f64,\n  /// The egress in bytes\n  pub egress_bytes: f64,\n}\n\npub fn sum_disk_usage(disks: &[SingleDiskUsage]) -> TotalDiskUsage {\n  disks\n    .iter()\n    .fold(TotalDiskUsage::default(), |mut total, disk| {\n      total.used_gb += disk.used_gb;\n      total.total_gb += disk.total_gb;\n      total\n    })\n}\n\n/// Info for the all system disks combined.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Default, Clone)]\npub struct TotalDiskUsage {\n  /// Used portion in GB\n  pub used_gb: f64,\n  /// Total size in GB\n  pub total_gb: f64,\n}\n\n/// Information about a process on the system.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct SystemProcess {\n  /// The process PID\n  pub pid: u32,\n  /// The process name\n  pub name: String,\n  /// The path to the process executable\n  #[serde(default)]\n  pub exe: String,\n  /// The command used to start the process\n  pub cmd: Vec<String>,\n  /// The time the process was started\n  #[serde(default)]\n  pub start_time: f64,\n  /// The cpu usage percentage of the process.\n  /// This is in core-percentage, eg 100% is 1 full core, and\n  /// an 8 core machine would max at 800%.\n  pub cpu_perc: f32,\n  /// The memory usage of the process in MB\n  pub mem_mb: f64,\n  /// Process disk read in KB/s\n  pub disk_read_kb: f64,\n  /// Process disk write in KB/s\n  pub disk_write_kb: f64,\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Default, Clone)]\npub struct SystemLoadAverage {\n  /// 1m load average\n  pub one: f64,\n  /// 5m load average\n  pub five: f64,\n  /// 15m load average\n  pub fifteen: f64,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/sync.rs",
    "content": "use bson::{Document, doc};\nuse derive_builder::Builder;\nuse derive_default_builder::DefaultBuilder;\nuse partial_derive2::Partial;\nuse serde::{Deserialize, Serialize};\nuse strum::Display;\nuse typeshare::typeshare;\n\nuse crate::deserializers::{\n  file_contents_deserializer, option_file_contents_deserializer,\n  option_string_list_deserializer, string_list_deserializer,\n};\n\nuse super::{\n  I64, ResourceTarget,\n  resource::{Resource, ResourceListItem, ResourceQuery},\n};\n\n#[typeshare]\npub type ResourceSyncListItem =\n  ResourceListItem<ResourceSyncListItemInfo>;\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ResourceSyncListItemInfo {\n  /// Unix timestamp of last sync, or 0\n  pub last_sync_ts: I64,\n  /// Whether sync is `files_on_host` mode.\n  pub files_on_host: bool,\n  /// Whether sync has file contents defined.\n  pub file_contents: bool,\n  /// Whether sync has `managed` mode enabled.\n  pub managed: bool,\n  /// Resource paths to the files.\n  pub resource_path: Vec<String>,\n  /// Linked repo, if one is attached.\n  pub linked_repo: String,\n  /// The git provider domain.\n  pub git_provider: String,\n  /// The Github repo used as the source of the sync resources\n  pub repo: String,\n  /// The branch of the repo\n  pub branch: String,\n  /// Full link to the repo.\n  pub repo_link: String,\n  /// Short commit hash of last sync, or empty string\n  pub last_sync_hash: Option<String>,\n  /// Commit message of last sync, or empty string\n  pub last_sync_message: Option<String>,\n  /// State of the sync. Reflects whether most recent sync successful.\n  pub state: ResourceSyncState,\n}\n\n#[typeshare]\n#[derive(\n  Debug,\n  Clone,\n  Copy,\n  Default,\n  PartialEq,\n  Eq,\n  PartialOrd,\n  Ord,\n  Serialize,\n  Deserialize,\n  Display,\n)]\npub enum ResourceSyncState {\n  /// Currently syncing\n  Syncing,\n  /// Updates pending\n  Pending,\n  /// Last sync successful (or never synced). No Changes pending\n  Ok,\n  /// Last sync failed\n  Failed,\n  /// Other case\n  #[default]\n  Unknown,\n}\n\n#[typeshare]\npub type ResourceSync =\n  Resource<ResourceSyncConfig, ResourceSyncInfo>;\n\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct ResourceSyncInfo {\n  /// Unix timestamp of last applied sync\n  #[serde(default)]\n  pub last_sync_ts: I64,\n  /// Short commit hash of last applied sync\n  pub last_sync_hash: Option<String>,\n  /// Commit message of last applied sync\n  pub last_sync_message: Option<String>,\n\n  /// The list of pending updates to resources\n  #[serde(default)]\n  pub resource_updates: Vec<ResourceDiff>,\n  /// The list of pending updates to variables\n  #[serde(default)]\n  pub variable_updates: Vec<DiffData>,\n  /// The list of pending updates to user groups\n  #[serde(default)]\n  pub user_group_updates: Vec<DiffData>,\n  /// The list of pending deploys to resources.\n  #[serde(default)]\n  pub pending_deploy: SyncDeployUpdate,\n  /// If there is an error, it will be stored here\n  pub pending_error: Option<String>,\n  /// The commit hash which produced these pending updates.\n  pub pending_hash: Option<String>,\n  /// The commit message which produced these pending updates.\n  pub pending_message: Option<String>,\n\n  /// The current sync files\n  #[serde(default)]\n  pub remote_contents: Vec<SyncFileContents>,\n  /// Any read errors in files by path\n  #[serde(default)]\n  pub remote_errors: Vec<SyncFileContents>,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ResourceDiff {\n  /// The resource target.\n  /// The target id will be empty if \"Create\" ResourceDiffType.\n  pub target: ResourceTarget,\n  /// The data associated with the diff.\n  pub data: DiffData,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", content = \"data\")]\npub enum DiffData {\n  /// Resource will be created\n  Create {\n    /// The name of resource to create\n    #[serde(default)]\n    name: String,\n    /// The proposed resource to create in TOML\n    proposed: String,\n  },\n  Update {\n    /// The proposed TOML\n    proposed: String,\n    /// The current TOML\n    current: String,\n  },\n  Delete {\n    /// The current TOML of the resource to delete\n    current: String,\n  },\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct SyncDeployUpdate {\n  /// Resources to deploy\n  pub to_deploy: i32,\n  /// A readable log of all the changes to be applied\n  pub log: String,\n}\n\n#[typeshare(serialized_as = \"Partial<ResourceSyncConfig>\")]\npub type _PartialResourceSyncConfig = PartialResourceSyncConfig;\n\n/// The sync configuration.\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize, Builder, Partial)]\n#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)]\n#[partial(skip_serializing_none, from, diff)]\npub struct ResourceSyncConfig {\n  /// Choose a Komodo Repo (Resource) to source the sync files.\n  #[serde(default)]\n  #[builder(default)]\n  pub linked_repo: String,\n\n  /// The git provider domain. Default: github.com\n  #[serde(default = \"default_git_provider\")]\n  #[builder(default = \"default_git_provider()\")]\n  #[partial_default(default_git_provider())]\n  pub git_provider: String,\n\n  /// Whether to use https to clone the repo (versus http). Default: true\n  ///\n  /// Note. Komodo does not currently support cloning repos via ssh.\n  #[serde(default = \"default_git_https\")]\n  #[builder(default = \"default_git_https()\")]\n  #[partial_default(default_git_https())]\n  pub git_https: bool,\n\n  /// The Github repo used as the source of the build.\n  #[serde(default)]\n  #[builder(default)]\n  pub repo: String,\n\n  /// The branch of the repo.\n  #[serde(default = \"default_branch\")]\n  #[builder(default = \"default_branch()\")]\n  #[partial_default(default_branch())]\n  pub branch: String,\n\n  /// Optionally set a specific commit hash.\n  #[serde(default)]\n  #[builder(default)]\n  pub commit: String,\n\n  /// The git account used to access private repos.\n  /// Passing empty string can only clone public repos.\n  ///\n  /// Note. A token for the account must be available in the core config or the builder server's periphery config\n  /// for the configured git provider.\n  #[serde(default)]\n  #[builder(default)]\n  pub git_account: String,\n\n  /// Whether incoming webhooks actually trigger action.\n  #[serde(default = \"default_webhook_enabled\")]\n  #[builder(default = \"default_webhook_enabled()\")]\n  #[partial_default(default_webhook_enabled())]\n  pub webhook_enabled: bool,\n\n  /// Optionally provide an alternate webhook secret for this sync.\n  /// If its an empty string, use the default secret from the config.\n  #[serde(default)]\n  #[builder(default)]\n  pub webhook_secret: String,\n\n  /// Files are available on the Komodo Core host.\n  /// Specify the file / folder with [ResourceSyncConfig::resource_path].\n  #[serde(default)]\n  #[builder(default)]\n  pub files_on_host: bool,\n\n  /// The path of the resource file(s) to sync.\n  ///  - If Files on Host, this is relative to the configured `sync_directory` in core config.\n  ///  - If Git Repo based, this is relative to the root of the repo.\n  /// Can be a specific file, or a directory containing multiple files / folders.\n  /// See [https://komo.do/docs/sync-resources](https://komo.do/docs/sync-resources) for more information.\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub resource_path: Vec<String>,\n\n  /// Enable \"pushes\" to the file,\n  /// which exports resources matching tags to single file.\n  ///  - If using `files_on_host`, it is stored in the file_contents, which must point to a .toml file path (it will be created if it doesn't exist).\n  ///  - If using `file_contents`, it is stored in the database.\n  /// When using this, \"delete\" mode is always enabled.\n  #[serde(default)]\n  #[builder(default)]\n  pub managed: bool,\n\n  /// Whether sync should delete resources\n  /// not declared in the resource files\n  #[serde(default)]\n  #[builder(default)]\n  pub delete: bool,\n\n  /// Whether sync should include resources.\n  /// Default: true\n  #[serde(default = \"default_include_resources\")]\n  #[builder(default = \"default_include_resources()\")]\n  #[partial_default(default_include_resources())]\n  pub include_resources: bool,\n\n  /// When using `managed` resource sync, will only export resources\n  /// matching all of the given tags. If none, will match all resources.\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_string_list_deserializer\"\n  ))]\n  #[builder(default)]\n  pub match_tags: Vec<String>,\n\n  /// Whether sync should include variables.\n  #[serde(default)]\n  #[builder(default)]\n  pub include_variables: bool,\n\n  /// Whether sync should include user groups.\n  #[serde(default)]\n  #[builder(default)]\n  pub include_user_groups: bool,\n\n  /// Whether sync should send alert when it enters Pending state.\n  /// Default: true\n  #[serde(default = \"default_pending_alert\")]\n  #[builder(default = \"default_pending_alert()\")]\n  #[partial_default(default_pending_alert())]\n  pub pending_alert: bool,\n\n  /// Manage the file contents in the UI.\n  #[serde(default, deserialize_with = \"file_contents_deserializer\")]\n  #[partial_attr(serde(\n    default,\n    deserialize_with = \"option_file_contents_deserializer\"\n  ))]\n  #[builder(default)]\n  pub file_contents: String,\n}\n\nimpl ResourceSyncConfig {\n  pub fn builder() -> ResourceSyncConfigBuilder {\n    ResourceSyncConfigBuilder::default()\n  }\n\n  /// Checks for empty file contents, ignoring whitespace / comments.\n  pub fn file_contents_empty(&self) -> bool {\n    self\n      .file_contents\n      .split('\\n')\n      .map(str::trim)\n      .filter(|line| !line.is_empty() && !line.starts_with('#'))\n      .count()\n      == 0\n  }\n}\n\nfn default_git_provider() -> String {\n  String::from(\"github.com\")\n}\n\nfn default_git_https() -> bool {\n  true\n}\n\nfn default_branch() -> String {\n  String::from(\"main\")\n}\n\nfn default_webhook_enabled() -> bool {\n  true\n}\n\nfn default_include_resources() -> bool {\n  true\n}\n\nfn default_pending_alert() -> bool {\n  true\n}\n\nimpl Default for ResourceSyncConfig {\n  fn default() -> Self {\n    Self {\n      linked_repo: Default::default(),\n      git_provider: default_git_provider(),\n      git_https: default_git_https(),\n      repo: Default::default(),\n      branch: default_branch(),\n      commit: Default::default(),\n      git_account: Default::default(),\n      resource_path: Default::default(),\n      files_on_host: Default::default(),\n      file_contents: Default::default(),\n      managed: Default::default(),\n      include_resources: default_include_resources(),\n      match_tags: Default::default(),\n      include_variables: Default::default(),\n      include_user_groups: Default::default(),\n      delete: Default::default(),\n      webhook_enabled: default_webhook_enabled(),\n      webhook_secret: Default::default(),\n      pending_alert: default_pending_alert(),\n    }\n  }\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct SyncFileContents {\n  /// The base resource path.\n  #[serde(default)]\n  pub resource_path: String,\n  /// The path of the file / error path relative to the resource path.\n  pub path: String,\n  /// The contents of the file\n  pub contents: String,\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]\npub struct ResourceSyncActionState {\n  /// Whether sync currently syncing\n  pub syncing: bool,\n}\n\n#[typeshare]\npub type ResourceSyncQuery =\n  ResourceQuery<ResourceSyncQuerySpecifics>;\n\n#[typeshare]\n#[derive(\n  Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,\n)]\npub struct ResourceSyncQuerySpecifics {\n  /// Filter syncs by their repo.\n  pub repos: Vec<String>,\n}\n\nimpl super::resource::AddFilters for ResourceSyncQuerySpecifics {\n  fn add_filters(&self, filters: &mut Document) {\n    if !self.repos.is_empty() {\n      filters.insert(\"config.repo\", doc! { \"$in\": &self.repos });\n    }\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/tag.rs",
    "content": "use derive_builder::Builder;\nuse partial_derive2::Partial;\nuse serde::{Deserialize, Serialize};\nuse strum::AsRefStr;\nuse typeshare::typeshare;\n\nuse crate::entities::MongoId;\n\n#[typeshare(serialized_as = \"Partial<Tag>\")]\npub type _PartialTag = PartialTag;\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]\n#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\npub struct Tag {\n  /// The Mongo ID of the tag.\n  /// This field is de/serialized from/to JSON as\n  /// `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Tag) }`\n  #[serde(\n    default,\n    rename = \"_id\",\n    skip_serializing_if = \"String::is_empty\",\n    with = \"bson::serde_helpers::hex_string_as_object_id\"\n  )]\n  #[builder(setter(skip))]\n  pub id: MongoId,\n\n  #[cfg_attr(feature = \"mongo\", unique_index)]\n  pub name: String,\n\n  #[serde(default)]\n  #[builder(default)]\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub owner: String,\n\n  /// Hex color code with alpha for UI display\n  #[serde(default)]\n  #[builder(default)]\n  pub color: TagColor,\n  // /// This field is not stored on database,\n  // /// but rather populated at query time based on results from the other resources.\n  // #[serde(default, skip_serializing_if = \"is_false\")]\n  // #[builder(default)]\n  // pub unused: bool,\n}\n\n// fn is_false(b: &bool) -> bool {\n//   !b\n// }\n\nimpl Tag {\n  pub fn builder() -> TagBuilder {\n    TagBuilder::default()\n  }\n}\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Default, Debug, Clone, AsRefStr)]\npub enum TagColor {\n  LightSlate,\n  #[default]\n  Slate,\n  DarkSlate,\n\n  LightRed,\n  Red,\n  DarkRed,\n\n  LightOrange,\n  Orange,\n  DarkOrange,\n\n  LightAmber,\n  Amber,\n  DarkAmber,\n\n  LightYellow,\n  Yellow,\n  DarkYellow,\n\n  LightLime,\n  Lime,\n  DarkLime,\n\n  LightGreen,\n  Green,\n  DarkGreen,\n\n  LightEmerald,\n  Emerald,\n  DarkEmerald,\n\n  LightTeal,\n  Teal,\n  DarkTeal,\n\n  LightCyan,\n  Cyan,\n  DarkCyan,\n\n  LightSky,\n  Sky,\n  DarkSky,\n\n  LightBlue,\n  Blue,\n  DarkBlue,\n\n  LightIndigo,\n  Indigo,\n  DarkIndigo,\n\n  LightViolet,\n  Violet,\n  DarkViolet,\n\n  LightPurple,\n  Purple,\n  DarkPurple,\n\n  LightFuchsia,\n  Fuchsia,\n  DarkFuchsia,\n\n  LightPink,\n  Pink,\n  DarkPink,\n\n  LightRose,\n  Rose,\n  DarkRose,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/toml.rs",
    "content": "use indexmap::{IndexMap, IndexSet};\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse super::{\n  ResourceTarget, ResourceTargetVariant,\n  action::_PartialActionConfig,\n  alerter::_PartialAlerterConfig,\n  build::_PartialBuildConfig,\n  builder::_PartialBuilderConfig,\n  deployment::_PartialDeploymentConfig,\n  permission::{\n    PermissionLevel, PermissionLevelAndSpecifics, SpecificPermission,\n  },\n  procedure::_PartialProcedureConfig,\n  repo::_PartialRepoConfig,\n  server::_PartialServerConfig,\n  stack::_PartialStackConfig,\n  sync::_PartialResourceSyncConfig,\n  variable::Variable,\n};\n\n/// Specifies resources to sync on Komodo\n#[typeshare]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct ResourcesToml {\n  #[serde(\n    default,\n    alias = \"server\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub servers: Vec<ResourceToml<_PartialServerConfig>>,\n\n  #[serde(\n    default,\n    alias = \"deployment\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub deployments: Vec<ResourceToml<_PartialDeploymentConfig>>,\n\n  #[serde(\n    default,\n    alias = \"stack\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub stacks: Vec<ResourceToml<_PartialStackConfig>>,\n\n  #[serde(\n    default,\n    alias = \"build\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub builds: Vec<ResourceToml<_PartialBuildConfig>>,\n\n  #[serde(\n    default,\n    alias = \"repo\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub repos: Vec<ResourceToml<_PartialRepoConfig>>,\n\n  #[serde(\n    default,\n    alias = \"procedure\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub procedures: Vec<ResourceToml<_PartialProcedureConfig>>,\n\n  #[serde(\n    default,\n    alias = \"action\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub actions: Vec<ResourceToml<_PartialActionConfig>>,\n\n  #[serde(\n    default,\n    alias = \"alerter\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub alerters: Vec<ResourceToml<_PartialAlerterConfig>>,\n\n  #[serde(\n    default,\n    alias = \"builder\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub builders: Vec<ResourceToml<_PartialBuilderConfig>>,\n\n  #[serde(\n    default,\n    alias = \"resource_sync\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub resource_syncs: Vec<ResourceToml<_PartialResourceSyncConfig>>,\n\n  #[serde(\n    default,\n    alias = \"user_group\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub user_groups: Vec<UserGroupToml>,\n\n  #[serde(\n    default,\n    alias = \"variable\",\n    skip_serializing_if = \"Vec::is_empty\"\n  )]\n  pub variables: Vec<Variable>,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ResourceToml<PartialConfig: Default> {\n  /// The resource name. Required\n  pub name: String,\n\n  /// The resource description. Optional.\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub description: String,\n\n  /// Mark resource as a template\n  #[serde(default, skip_serializing_if = \"is_false\")]\n  pub template: bool,\n\n  /// Tag ids or names. Optional\n  #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n  pub tags: Vec<String>,\n\n  /// Optional. Only relevant for deployments / stacks.\n  ///\n  /// Will ensure deployment / stack is running with the latest configuration.\n  /// Deploy actions to achieve this will be included in the sync.\n  /// Default is false.\n  #[serde(default, skip_serializing_if = \"is_false\")]\n  pub deploy: bool,\n\n  /// Optional. Only relevant for deployments / stacks using the 'deploy' sync feature.\n  ///\n  /// Specify other deployments / stacks by name as dependencies.\n  /// The sync will ensure the deployment / stack will only be deployed 'after' its dependencies.\n  #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n  pub after: Vec<String>,\n\n  /// Resource specific configuration.\n  #[serde(default)]\n  pub config: PartialConfig,\n}\n\nfn is_false(b: &bool) -> bool {\n  !b\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct UserGroupToml {\n  /// User group name\n  pub name: String,\n\n  /// Whether all users will implicitly have the permissions in this group.\n  #[serde(default)]\n  pub everyone: bool,\n\n  /// Users in the group\n  #[serde(default)]\n  pub users: Vec<String>,\n\n  /// Give the user group elevated permissions on all resources of a certain type\n  #[serde(default)]\n  pub all:\n    IndexMap<ResourceTargetVariant, PermissionLevelAndSpecifics>,\n\n  /// Permissions given to the group\n  #[serde(default, alias = \"permission\")]\n  pub permissions: Vec<PermissionToml>,\n}\n\n#[typeshare]\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct PermissionToml {\n  /// Id can be:\n  ///   - resource name. `id = \"abcd-build\"`\n  ///   - regex matching resource names. `id = \"\\^(.+)-build-([0-9]+)$\\\"`\n  pub target: ResourceTarget,\n\n  /// The permission level:\n  ///   - None\n  ///   - Read\n  ///   - Execute\n  ///   - Write\n  #[serde(default)]\n  pub level: PermissionLevel,\n\n  /// Any [SpecificPermissions](SpecificPermission) on the resource\n  #[serde(default, skip_serializing_if = \"IndexSet::is_empty\")]\n  pub specific: IndexSet<SpecificPermission>,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/update.rs",
    "content": "use async_timing_util::unix_timestamp_ms;\nuse serde::{Deserialize, Serialize};\nuse strum::{Display, EnumString};\nuse typeshare::typeshare;\n\nuse crate::entities::{\n  I64, MongoId, Operation, all_logs_success, komodo_timestamp,\n};\n\nuse super::{ResourceTarget, Version};\n\n/// Represents an action performed by Komodo.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\n#[cfg_attr(feature = \"mongo\", doc_index({ \"target.type\": 1 }))]\n#[cfg_attr(feature = \"mongo\", sparse_doc_index({ \"target.id\": 1 }))]\npub struct Update {\n  /// The Mongo ID of the update.\n  /// This field is de/serialized from/to JSON as\n  /// `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Update) }`\n  #[serde(\n    default,\n    rename = \"_id\",\n    skip_serializing_if = \"String::is_empty\",\n    with = \"bson::serde_helpers::hex_string_as_object_id\"\n  )]\n  pub id: MongoId,\n\n  /// The operation performed\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub operation: Operation,\n\n  /// The time the operation started\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub start_ts: I64,\n\n  /// Whether the operation was successful\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub success: bool,\n\n  /// The user id that triggered the update.\n  ///\n  /// Also can take these values for operations triggered automatically:\n  /// - `Procedure`: The operation was triggered as part of a procedure run\n  /// - `Github`: The operation was triggered by a github webhook\n  /// - `Auto Redeploy`: The operation (always `Deploy`) was triggered by an attached build finishing.\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub operator: String,\n\n  /// The target resource to which this update refers\n  pub target: ResourceTarget,\n  /// Logs produced as the operation is performed\n  pub logs: Vec<Log>,\n  /// The time the operation completed.\n  pub end_ts: Option<I64>,\n  /// The status of the update\n  /// - `Queued`\n  /// - `InProgress`\n  /// - `Complete`\n  #[cfg_attr(feature = \"mongo\", index)]\n  pub status: UpdateStatus,\n  /// An optional version on the update, ie build version or deployed version.\n  #[serde(default, skip_serializing_if = \"Version::is_none\")]\n  pub version: Version,\n  /// An optional commit hash associated with the update, ie cloned hash or deployed hash.\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub commit_hash: String,\n  /// Some unstructured, operation specific data. Not for general usage.\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub other_data: String,\n  /// If the update is for resource config update, give the previous toml contents\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub prev_toml: String,\n  /// If the update is for resource config update, give the current (at time of Update) toml contents\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub current_toml: String,\n}\n\nimpl Update {\n  pub fn push_simple_log(\n    &mut self,\n    stage: &str,\n    msg: impl Into<String>,\n  ) {\n    self.logs.push(Log::simple(stage, msg.into()));\n  }\n\n  pub fn push_error_log(\n    &mut self,\n    stage: &str,\n    msg: impl Into<String>,\n  ) {\n    self.logs.push(Log::error(stage, msg.into()));\n  }\n\n  pub fn in_progress(&mut self) {\n    self.status = UpdateStatus::InProgress;\n  }\n\n  pub fn finalize(&mut self) {\n    self.success = all_logs_success(&self.logs);\n    self.end_ts = Some(komodo_timestamp());\n    self.status = UpdateStatus::Complete;\n  }\n}\n\n/// Minimal representation of an action performed by Komodo.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct UpdateListItem {\n  /// The id of the update\n  pub id: String,\n  /// Which operation was run\n  pub operation: Operation,\n  /// The starting time of the operation\n  pub start_ts: I64,\n  /// Whether the operation was successful\n  pub success: bool,\n  /// The username of the user performing update\n  pub username: String,\n  /// The user id that triggered the update.\n  ///\n  /// Also can take these values for operations triggered automatically:\n  /// - `Procedure`: The operation was triggered as part of a procedure run\n  /// - `Github`: The operation was triggered by a github webhook\n  /// - `Auto Redeploy`: The operation (always `Deploy`) was triggered by an attached build finishing.\n  pub operator: String,\n  /// The target resource to which this update refers\n  pub target: ResourceTarget,\n  /// The status of the update\n  /// - `Queued`\n  /// - `InProgress`\n  /// - `Complete`\n  pub status: UpdateStatus,\n  /// An optional version on the update, ie build version or deployed version.\n  #[serde(default, skip_serializing_if = \"Version::is_none\")]\n  pub version: Version,\n  /// Some unstructured, operation specific data. Not for general usage.\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub other_data: String,\n}\n\n/// Represents the output of some command being run\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct Log {\n  /// A label for the log\n  pub stage: String,\n  /// The command which was executed\n  pub command: String,\n  /// The output of the command in the standard channel\n  pub stdout: String,\n  /// The output of the command in the error channel\n  pub stderr: String,\n  /// Whether the command run was successful\n  pub success: bool,\n  /// The start time of the command execution\n  pub start_ts: I64,\n  /// The end time of the command execution\n  pub end_ts: I64,\n}\n\nimpl Log {\n  pub fn simple(stage: &str, msg: String) -> Log {\n    let ts = unix_timestamp_ms() as i64;\n    Log {\n      stage: stage.to_string(),\n      stdout: msg,\n      success: true,\n      start_ts: ts,\n      end_ts: ts,\n      ..Default::default()\n    }\n  }\n\n  pub fn error(stage: &str, msg: String) -> Log {\n    let ts = unix_timestamp_ms() as i64;\n    Log {\n      stage: stage.to_string(),\n      stderr: msg,\n      start_ts: ts,\n      end_ts: ts,\n      success: false,\n      ..Default::default()\n    }\n  }\n\n  /// Combines stdout / stderr into one log\n  pub fn combined(&self) -> String {\n    match (self.stdout.is_empty(), self.stderr.is_empty()) {\n      (true, true) => {\n        format!(\"stdout: {}\\n\\nstderr: {}\", self.stdout, self.stderr)\n      }\n      (true, false) => self.stdout.to_string(),\n      (false, true) => self.stderr.to_string(),\n      (false, false) => String::from(\"No log\"),\n    }\n  }\n}\n\n/// An update's status\n#[typeshare]\n#[derive(\n  Serialize,\n  Deserialize,\n  Debug,\n  Display,\n  EnumString,\n  PartialEq,\n  Hash,\n  Eq,\n  Clone,\n  Copy,\n  Default,\n)]\npub enum UpdateStatus {\n  /// The run is in the system but hasn't started yet\n  Queued,\n  /// The run is currently running\n  InProgress,\n  /// The run is complete\n  #[default]\n  Complete,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/user.rs",
    "content": "use std::{collections::HashMap, sync::OnceLock};\n\nuse indexmap::IndexMap;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::entities::{I64, MongoId};\n\nuse super::{\n  ResourceTargetVariant, permission::PermissionLevelAndSpecifics,\n};\n\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\n#[cfg_attr(feature = \"mongo\", doc_index({ \"config.type\": 1 }))]\n#[cfg_attr(feature = \"mongo\", sparse_doc_index({ \"config.data.google_id\": 1 }))]\n#[cfg_attr(feature = \"mongo\", sparse_doc_index({ \"config.data.github_id\": 1 }))]\npub struct User {\n  /// The Mongo ID of the User.\n  /// This field is de/serialized from/to JSON as\n  /// `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of User schema) }`\n  #[serde(\n    default,\n    rename = \"_id\",\n    skip_serializing_if = \"String::is_empty\",\n    with = \"bson::serde_helpers::hex_string_as_object_id\"\n  )]\n  pub id: MongoId,\n\n  /// The globally unique username for the user.\n  #[cfg_attr(feature = \"mongo\", unique_index)]\n  pub username: String,\n\n  /// Whether user is enabled / able to access the api.\n  #[cfg_attr(feature = \"mongo\", index)]\n  #[serde(default)]\n  pub enabled: bool,\n\n  /// Can give / take other users admin priviledges.\n  #[serde(default)]\n  pub super_admin: bool,\n\n  /// Whether the user has global admin permissions.\n  #[serde(default)]\n  pub admin: bool,\n\n  /// Whether the user has permission to create servers.\n  #[serde(default)]\n  pub create_server_permissions: bool,\n\n  /// Whether the user has permission to create builds\n  #[serde(default)]\n  pub create_build_permissions: bool,\n\n  /// The user-type specific config.\n  pub config: UserConfig,\n\n  /// When the user last opened updates dropdown.\n  #[serde(default)]\n  pub last_update_view: I64,\n\n  /// Recently viewed ids\n  #[serde(default)]\n  pub recents: HashMap<ResourceTargetVariant, Vec<String>>,\n\n  /// Give the user elevated permissions on all resources of a certain type\n  #[serde(default)]\n  pub all:\n    IndexMap<ResourceTargetVariant, PermissionLevelAndSpecifics>,\n\n  #[serde(default)]\n  pub updated_at: I64,\n}\n\nimpl User {\n  /// Prepares user object for transport by removing any sensitive fields\n  pub fn sanitize(&mut self) {\n    if let UserConfig::Local { .. } = &self.config {\n      self.config = UserConfig::default();\n    }\n  }\n\n  /// Returns whether user is an inbuilt service user\n  ///\n  /// NOTE: ALSO UPDATE `frontend/src/lib/utils/is_service_user` to match\n  pub fn is_service_user(user_id: &str) -> bool {\n    matches!(\n      user_id,\n      \"System\"\n        | \"000000000000000000000000\"\n        | \"Procedure\"\n        | \"000000000000000000000001\"\n        | \"Action\"\n        | \"000000000000000000000002\"\n        | \"Git Webhook\"\n        | \"000000000000000000000003\"\n        | \"Auto Redeploy\"\n        | \"000000000000000000000004\"\n        | \"Resource Sync\"\n        | \"000000000000000000000005\"\n        | \"Stack Wizard\"\n        | \"000000000000000000000006\"\n        | \"Build Manager\"\n        | \"000000000000000000000007\"\n        | \"Repo Manager\"\n        | \"000000000000000000000008\"\n    )\n  }\n}\n\npub fn admin_service_user(user_id: &str) -> Option<User> {\n  match user_id {\n    \"000000000000000000000000\" | \"System\" => {\n      system_user().to_owned().into()\n    }\n    \"000000000000000000000001\" | \"Procedure\" => {\n      procedure_user().to_owned().into()\n    }\n    \"000000000000000000000002\" | \"Action\" => {\n      action_user().to_owned().into()\n    }\n    \"000000000000000000000003\" | \"Git Webhook\" => {\n      git_webhook_user().to_owned().into()\n    }\n    \"000000000000000000000004\" | \"Auto Redeploy\" => {\n      auto_redeploy_user().to_owned().into()\n    }\n    \"000000000000000000000005\" | \"Resource Sync\" => {\n      sync_user().to_owned().into()\n    }\n    \"000000000000000000000006\" | \"Stack Wizard\" => {\n      stack_user().to_owned().into()\n    }\n    \"000000000000000000000007\" | \"Build Manager\" => {\n      build_user().to_owned().into()\n    }\n    \"000000000000000000000008\" | \"Repo Manager\" => {\n      repo_user().to_owned().into()\n    }\n    _ => None,\n  }\n}\n\npub fn system_user() -> &'static User {\n  static SYSTEM_USER: OnceLock<User> = OnceLock::new();\n  SYSTEM_USER.get_or_init(|| {\n    let id_name = String::from(\"System\");\n    User {\n      id: \"000000000000000000000000\".to_string(),\n      username: id_name,\n      enabled: true,\n      admin: true,\n      ..Default::default()\n    }\n  })\n}\n\npub fn procedure_user() -> &'static User {\n  static PROCEDURE_USER: OnceLock<User> = OnceLock::new();\n  PROCEDURE_USER.get_or_init(|| {\n    let id_name = String::from(\"Procedure\");\n    User {\n      id: \"000000000000000000000001\".to_string(),\n      username: id_name,\n      enabled: true,\n      admin: true,\n      ..Default::default()\n    }\n  })\n}\n\npub fn action_user() -> &'static User {\n  static ACTION_USER: OnceLock<User> = OnceLock::new();\n  ACTION_USER.get_or_init(|| {\n    let id_name = String::from(\"Action\");\n    User {\n      id: \"000000000000000000000002\".to_string(),\n      username: id_name,\n      enabled: true,\n      admin: true,\n      ..Default::default()\n    }\n  })\n}\n\npub fn git_webhook_user() -> &'static User {\n  static GIT_WEBHOOK_USER: OnceLock<User> = OnceLock::new();\n  GIT_WEBHOOK_USER.get_or_init(|| {\n    let id_name = String::from(\"Git Webhook\");\n    User {\n      id: \"000000000000000000000003\".to_string(),\n      username: id_name,\n      enabled: true,\n      admin: true,\n      ..Default::default()\n    }\n  })\n}\n\npub fn auto_redeploy_user() -> &'static User {\n  static AUTO_REDEPLOY_USER: OnceLock<User> = OnceLock::new();\n  AUTO_REDEPLOY_USER.get_or_init(|| {\n    let id_name = String::from(\"Auto Redeploy\");\n    User {\n      id: \"000000000000000000000004\".to_string(),\n      username: id_name,\n      enabled: true,\n      admin: true,\n      ..Default::default()\n    }\n  })\n}\n\npub fn sync_user() -> &'static User {\n  static SYNC_USER: OnceLock<User> = OnceLock::new();\n  SYNC_USER.get_or_init(|| {\n    let id_name = String::from(\"Resource Sync\");\n    User {\n      id: \"000000000000000000000005\".to_string(),\n      username: id_name,\n      enabled: true,\n      admin: true,\n      ..Default::default()\n    }\n  })\n}\n\npub fn stack_user() -> &'static User {\n  static STACK_USER: OnceLock<User> = OnceLock::new();\n  STACK_USER.get_or_init(|| {\n    let id_name = String::from(\"Stack Wizard\");\n    User {\n      id: \"000000000000000000000006\".to_string(),\n      username: id_name,\n      enabled: true,\n      admin: true,\n      ..Default::default()\n    }\n  })\n}\n\npub fn build_user() -> &'static User {\n  static BUILD_USER: OnceLock<User> = OnceLock::new();\n  BUILD_USER.get_or_init(|| {\n    let id_name = String::from(\"Build Manager\");\n    User {\n      id: \"000000000000000000000007\".to_string(),\n      username: id_name,\n      enabled: true,\n      admin: true,\n      ..Default::default()\n    }\n  })\n}\n\npub fn repo_user() -> &'static User {\n  static REPO_USER: OnceLock<User> = OnceLock::new();\n  REPO_USER.get_or_init(|| {\n    let id_name = String::from(\"Repo Manager\");\n    User {\n      id: \"000000000000000000000008\".to_string(),\n      username: id_name,\n      enabled: true,\n      admin: true,\n      ..Default::default()\n    }\n  })\n}\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", content = \"data\")]\npub enum UserConfig {\n  /// User that logs in with username / password\n  Local { password: String },\n\n  /// User that logs in via Google Oauth\n  Google { google_id: String, avatar: String },\n\n  /// User that logs in via Github Oauth\n  Github { github_id: String, avatar: String },\n\n  /// User that logs in via Oidc provider\n  Oidc { provider: String, user_id: String },\n\n  /// Non-human managed user, can have it's own permissions / api keys\n  Service { description: String },\n}\n\nimpl Default for UserConfig {\n  fn default() -> Self {\n    Self::Local {\n      password: String::new(),\n    }\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/user_group.rs",
    "content": "use indexmap::IndexMap;\nuse serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\nuse crate::deserializers::string_list_deserializer;\n\nuse super::{\n  I64, MongoId, ResourceTargetVariant,\n  permission::PermissionLevelAndSpecifics,\n};\n\n/// Permission users at the group level.\n///\n/// All users that are part of a group inherit the group's permissions.\n/// A user can be a part of multiple groups. A user's permission on a particular resource\n/// will be resolved to be the maximum permission level between the user's own permissions and\n/// any groups they are a part of.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\npub struct UserGroup {\n  /// The Mongo ID of the UserGroup.\n  /// This field is de/serialized from/to JSON as\n  /// `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized User) }`\n  #[serde(\n    default,\n    rename = \"_id\",\n    skip_serializing_if = \"String::is_empty\",\n    with = \"bson::serde_helpers::hex_string_as_object_id\"\n  )]\n  pub id: MongoId,\n\n  /// A name for the user group\n  #[cfg_attr(feature = \"mongo\", unique_index)]\n  pub name: String,\n\n  /// Whether all users will implicitly have the permissions in this group.\n  #[cfg_attr(feature = \"mongo\", index)]\n  #[serde(default)]\n  pub everyone: bool,\n\n  /// User ids of group members\n  #[cfg_attr(feature = \"mongo\", index)]\n  #[serde(default, deserialize_with = \"string_list_deserializer\")]\n  pub users: Vec<String>,\n\n  /// Give the user group elevated permissions on all resources of a certain type\n  #[serde(default)]\n  pub all:\n    IndexMap<ResourceTargetVariant, PermissionLevelAndSpecifics>,\n\n  /// Unix time (ms) when user group last updated\n  #[serde(default)]\n  pub updated_at: I64,\n}\n"
  },
  {
    "path": "client/core/rs/src/entities/variable.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse typeshare::typeshare;\n\n/// A non-secret global variable which can be interpolated into deployment\n/// environment variable values and build argument values.\n#[typeshare]\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\n#[cfg_attr(\n  feature = \"mongo\",\n  derive(mongo_indexed::derive::MongoIndexed)\n)]\npub struct Variable {\n  /// Unique name associated with the variable.\n  /// Instances of '[[variable.name]]' in value will be replaced with 'variable.value'.\n  #[cfg_attr(feature = \"mongo\", unique_index)]\n  pub name: String,\n  /// A description for the variable.\n  #[serde(default, skip_serializing_if = \"String::is_empty\")]\n  pub description: String,\n  /// The value associated with the variable.\n  #[serde(default)]\n  pub value: String,\n  /// If marked as secret, the variable value will be hidden in updates / logs.\n  /// Additionally the value will not be served in read requests by non admin users.\n  ///\n  /// Note that the value is NOT encrypted in the database, and will likely show up in database logs.\n  /// The security of these variables comes down to the security\n  /// of the database (system level encryption, network isolation, etc.)\n  #[serde(default)]\n  pub is_secret: bool,\n}\n"
  },
  {
    "path": "client/core/rs/src/lib.rs",
    "content": "//! # Komodo\n//! *A system to build and deploy software across many servers*. [**https://komo.do**](https://komo.do)\n//!\n//! This is a client library for the Komodo Core API.\n//! It contains:\n//! - Definitions for the application [api] and [entities].\n//! - A [client][KomodoClient] to interact with the Komodo Core API.\n//! - Information on configuring Komodo [Core][entities::config::core] and [Periphery][entities::config::periphery].\n//!\n//! ## Client Configuration\n//!\n//! The client includes a convenenience method to parse the Komodo API url and credentials from the environment:\n//! - `KOMODO_ADDRESS`\n//! - `KOMODO_API_KEY`\n//! - `KOMODO_API_SECRET`\n//!\n//! ## Client Example\n//! ```text\n//! dotenvy::dotenv().ok();\n//!\n//! let client = KomodoClient::new_from_env()?;\n//!\n//! // Get all the deployments\n//! let deployments = client.read(ListDeployments::default()).await?;\n//!\n//! println!(\"{deployments:#?}\");\n//!\n//! let update = client.execute(RunBuild { build: \"test-build\".to_string() }).await?:\n//! ```\n\nuse std::{sync::OnceLock, time::Duration};\n\nuse anyhow::Context;\nuse api::read::GetVersion;\nuse serde::Deserialize;\n\npub mod api;\npub mod busy;\npub mod deserializers;\npub mod entities;\npub mod parsers;\npub mod terminal;\npub mod ws;\n\nmod request;\n\n/// &'static KomodoClient initialized from environment.\npub fn komodo_client() -> &'static KomodoClient {\n  static KOMODO_CLIENT: OnceLock<KomodoClient> = OnceLock::new();\n  KOMODO_CLIENT.get_or_init(|| {\n    KomodoClient::new_from_env()\n      .context(\"Missing KOMODO_ADDRESS, KOMODO_API_KEY, KOMODO_API_SECRET from env\")\n      .unwrap()\n  })\n}\n\n/// Default environment variables for the [KomodoClient].\n#[derive(Deserialize)]\npub struct KomodoEnv {\n  /// KOMODO_ADDRESS\n  pub komodo_address: String,\n  /// KOMODO_API_KEY\n  pub komodo_api_key: String,\n  /// KOMODO_API_SECRET\n  pub komodo_api_secret: String,\n}\n\n/// Client to interface with [Komodo](https://komo.do/docs/api#rust-client)\n#[derive(Clone)]\npub struct KomodoClient {\n  #[cfg(not(feature = \"blocking\"))]\n  reqwest: reqwest::Client,\n  #[cfg(feature = \"blocking\")]\n  reqwest: reqwest::blocking::Client,\n  address: String,\n  key: String,\n  secret: String,\n}\n\nimpl KomodoClient {\n  /// Initializes KomodoClient, including a health check.\n  pub fn new(\n    address: impl Into<String>,\n    key: impl Into<String>,\n    secret: impl Into<String>,\n  ) -> KomodoClient {\n    KomodoClient {\n      reqwest: Default::default(),\n      address: address.into(),\n      key: key.into(),\n      secret: secret.into(),\n    }\n  }\n\n  /// Initializes KomodoClient from environment: [KomodoEnv]\n  pub fn new_from_env() -> anyhow::Result<KomodoClient> {\n    let KomodoEnv {\n      komodo_address,\n      komodo_api_key,\n      komodo_api_secret,\n    } = envy::from_env()\n      .context(\"failed to parse environment for komodo client\")?;\n    Ok(KomodoClient::new(\n      komodo_address,\n      komodo_api_key,\n      komodo_api_secret,\n    ))\n  }\n\n  /// Add a healthcheck in the initialization pipeline:\n  ///\n  /// ```text\n  /// let komodo = KomodoClient::new_from_env()?\n  ///   .with_healthcheck().await?;\n  /// ```\n  #[cfg(not(feature = \"blocking\"))]\n  pub async fn with_healthcheck(self) -> anyhow::Result<Self> {\n    self.health_check().await?;\n    Ok(self)\n  }\n\n  /// Add a healthcheck in the initialization pipeline:\n  ///\n  /// ```text\n  /// let komodo = KomodoClient::new_from_env()?\n  ///   .with_healthcheck().await?;\n  /// ```\n  #[cfg(feature = \"blocking\")]\n  pub fn with_healthcheck(self) -> anyhow::Result<Self> {\n    self.health_check()?;\n    Ok(self)\n  }\n\n  /// Get the Core version.\n  #[cfg(not(feature = \"blocking\"))]\n  pub async fn core_version(&self) -> anyhow::Result<String> {\n    self.read(GetVersion {}).await.map(|r| r.version)\n  }\n\n  /// Get the Core version.\n  #[cfg(feature = \"blocking\")]\n  pub fn core_version(&self) -> anyhow::Result<String> {\n    self.read(GetVersion {}).map(|r| r.version)\n  }\n\n  /// Send a health check.\n  #[cfg(not(feature = \"blocking\"))]\n  pub async fn health_check(&self) -> anyhow::Result<()> {\n    self.read(GetVersion {}).await.map(|_| ())\n  }\n\n  /// Send a health check.\n  #[cfg(feature = \"blocking\")]\n  pub fn health_check(&self) -> anyhow::Result<()> {\n    self.read(GetVersion {}).map(|_| ())\n  }\n\n  /// Use a custom reqwest client.\n  #[cfg(not(feature = \"blocking\"))]\n  pub fn set_reqwest(mut self, reqwest: reqwest::Client) -> Self {\n    self.reqwest = reqwest;\n    self\n  }\n\n  /// Use a custom reqwest client.\n  #[cfg(feature = \"blocking\")]\n  pub fn set_reqwest(\n    mut self,\n    reqwest: reqwest::blocking::Client,\n  ) -> Self {\n    self.reqwest = reqwest;\n    self\n  }\n\n  /// Poll an [Update][entities::update::Update] (returned by the `execute` calls) until the\n  /// [UpdateStatus][entities::update::UpdateStatus] is `Complete`, and then return it.\n  #[cfg(not(feature = \"blocking\"))]\n  pub async fn poll_update_until_complete(\n    &self,\n    update_id: impl Into<String>,\n  ) -> anyhow::Result<entities::update::Update> {\n    let update_id = update_id.into();\n    loop {\n      let update = self\n        .read(api::read::GetUpdate {\n          id: update_id.clone(),\n        })\n        .await?;\n      if update.status == entities::update::UpdateStatus::Complete {\n        return Ok(update);\n      }\n      tokio::time::sleep(Duration::from_millis(500)).await;\n    }\n  }\n\n  /// Poll an [Update][entities::update::Update] (returned by the `execute` calls) until the\n  /// [UpdateStatus][entities::update::UpdateStatus] is `Complete`, and then return it.\n  #[cfg(feature = \"blocking\")]\n  pub fn poll_update_until_complete(\n    &self,\n    update_id: impl Into<String>,\n  ) -> anyhow::Result<entities::update::Update> {\n    let update_id = update_id.into();\n    loop {\n      let update = self.read(api::read::GetUpdate {\n        id: update_id.clone(),\n      })?;\n      if update.status == entities::update::UpdateStatus::Complete {\n        return Ok(update);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/parsers.rs",
    "content": "use anyhow::Context;\n\npub const QUOTE_PATTERN: &[char] = &['\"', '\\''];\n\n/// Parses a list of key value pairs from a multiline string\n///\n/// Example source:\n/// ```text\n/// # Supports comments\n/// KEY_1 = value_1 # end of line comments\n///\n/// # Supports string wrapped values\n/// KEY_2=\"value_2\"\n/// 'KEY_3 = value_3'\n///\n/// # Also supports yaml list formats\n/// - KEY_4: 'value_4'\n/// - \"KEY_5=value_5\"\n///\n/// # Wrapping outer quotes are removed while inner quotes are preserved\n/// \"KEY_6 = 'value_6'\"\n/// ```\n///\n/// Note this preserves the wrapping string around value.\n/// Writing environment file should format the value exactly as it comes in,\n/// including the given wrapping quotes.\n///\n/// Returns:\n/// ```text\n/// [\n///   (\"KEY_1\", \"value_1\"),\n///   (\"KEY_2\", \"\\\"value_2\\\"\"),\n///   (\"KEY_3\", \"value_3\"),\n///   (\"KEY_4\", \"'value_4'\"),\n///   (\"KEY_5\", \"value_5\"),\n///   (\"KEY_6\", \"'value_6'\"),\n/// ]\n/// ```\npub fn parse_key_value_list(\n  input: &str,\n) -> anyhow::Result<Vec<(String, String)>> {\n  let trimmed = input.trim();\n  if trimmed.is_empty() {\n    return Ok(Vec::new());\n  }\n  trimmed\n    .split('\\n')\n    .map(|line| line.trim())\n    .enumerate()\n    .filter(|(_, line)| {\n      !line.is_empty()\n        && !line.starts_with('#')\n        && !line.starts_with(\"//\")\n    })\n    .map(|(i, line)| {\n      let line = line\n        // Remove end of line comments\n        .split_once(\" #\")\n        .unwrap_or((line, \"\"))\n        .0\n        .trim()\n        // Remove preceding '-' (yaml list)\n        .trim_start_matches('-')\n        .trim();\n      let (key, value) = line\n        .split_once(['=', ':'])\n        .with_context(|| {\n          format!(\n            \"line {i} missing assignment character ('=' or ':')\"\n          )\n        })\n        .map(|(key, value)| {\n          let key = key.trim();\n          let value = value.trim();\n\n          // Remove wrapping quotes when around key AND value\n          let (key, value) = if key.starts_with(QUOTE_PATTERN)\n            && !key.ends_with(QUOTE_PATTERN)\n            && value.ends_with(QUOTE_PATTERN)\n          {\n            (\n              key.strip_prefix(QUOTE_PATTERN).unwrap().trim(),\n              value.strip_suffix(QUOTE_PATTERN).unwrap().trim(),\n            )\n          } else {\n            (key, value)\n          };\n\n          (key.to_string(), value.to_string())\n        })?;\n      anyhow::Ok((key, value))\n    })\n    .collect::<anyhow::Result<Vec<_>>>()\n}\n\n/// Parses commands out of multiline string\n/// and chains them together with '&&'\n///\n/// Supports full line and end of line comments, and escaped newlines.\n///\n/// ## Example:\n/// ```sh\n/// # comments supported\n/// sh ./shell1.sh # end of line supported\n/// sh ./shell2.sh\n///\n/// # escaped newlines supported\n/// curl --header \"Content-Type: application/json\" \\\n///   --request POST \\\n///   --data '{\"key\": \"value\"}' \\\n///   https://destination.com\n///\n/// # print done\n/// echo done\n/// ```\n/// becomes\n/// ```sh\n/// sh ./shell1.sh && sh ./shell2.sh && {long curl command} && echo done\n/// ```\npub fn parse_multiline_command(command: impl AsRef<str>) -> String {\n  command\n    .as_ref()\n    // Remove comments and join back\n    .split('\\n')\n    .map(str::trim)\n    .filter(|line| !line.is_empty() && !line.starts_with('#'))\n    .filter_map(|line| line.split(\" #\").next())\n    .collect::<Vec<_>>()\n    .join(\"\\n\")\n    // Remove escaped newlines\n    .split(\" \\\\\")\n    .map(str::trim)\n    .fold(String::new(), |acc, el| acc + \" \" + el)\n    // Then final split by newlines and join with &&\n    .split('\\n')\n    .map(str::trim)\n    .filter(|line| !line.is_empty() && !line.starts_with('#'))\n    .filter_map(|line| line.split(\" #\").next())\n    .map(str::trim)\n    .collect::<Vec<_>>()\n    .join(\" && \")\n}\n\n/// Parses a list of strings from a comment seperated and multiline string\n///\n/// Example source:\n/// ```text\n/// # supports comments\n/// path/to/file1 # comment1\n/// path/to/file2\n///\n/// # also supports comma seperated values\n/// path/to/file3,path/to/file4\n/// ```\n///\n/// Returns:\n/// ```text\n/// [\"path/to/file1\", \"path/to/file2\", \"path/to/file3\", \"path/to/file4\"]\n/// ```\npub fn parse_string_list(source: impl AsRef<str>) -> Vec<String> {\n  source\n    .as_ref()\n    .split('\\n')\n    .map(str::trim)\n    .filter(|line| !line.is_empty() && !line.starts_with('#'))\n    .filter_map(|line| line.split(\" #\").next())\n    .flat_map(|line| line.split(','))\n    .map(str::trim)\n    .filter(|entry| !entry.is_empty())\n    .map(str::to_string)\n    .collect()\n}\n"
  },
  {
    "path": "client/core/rs/src/request.rs",
    "content": "use anyhow::{Context, anyhow};\nuse serde::{Serialize, de::DeserializeOwned};\nuse serde_json::json;\nuse serror::deserialize_error;\n\nuse crate::{\n  KomodoClient,\n  api::{\n    auth::KomodoAuthRequest, execute::KomodoExecuteRequest,\n    read::KomodoReadRequest, user::KomodoUserRequest,\n    write::KomodoWriteRequest,\n  },\n};\n\nimpl KomodoClient {\n  #[cfg(not(feature = \"blocking\"))]\n  pub async fn auth<T>(\n    &self,\n    request: T,\n  ) -> anyhow::Result<T::Response>\n  where\n    T: Serialize + KomodoAuthRequest,\n    T::Response: DeserializeOwned,\n  {\n    self\n      .post(\n        \"/auth\",\n        json!({\n          \"type\": T::req_type(),\n          \"params\": request\n        }),\n      )\n      .await\n  }\n\n  #[cfg(feature = \"blocking\")]\n  pub fn auth<T>(&self, request: T) -> anyhow::Result<T::Response>\n  where\n    T: Serialize + KomodoAuthRequest,\n    T::Response: DeserializeOwned,\n  {\n    self.post(\n      \"/auth\",\n      json!({\n        \"type\": T::req_type(),\n        \"params\": request\n      }),\n    )\n  }\n\n  #[cfg(not(feature = \"blocking\"))]\n  pub async fn user<T>(\n    &self,\n    request: T,\n  ) -> anyhow::Result<T::Response>\n  where\n    T: Serialize + KomodoUserRequest,\n    T::Response: DeserializeOwned,\n  {\n    self\n      .post(\n        \"/auth\",\n        json!({\n          \"type\": T::req_type(),\n          \"params\": request\n        }),\n      )\n      .await\n  }\n\n  #[cfg(feature = \"blocking\")]\n  pub fn user<T>(&self, request: T) -> anyhow::Result<T::Response>\n  where\n    T: Serialize + KomodoUserRequest,\n    T::Response: DeserializeOwned,\n  {\n    self.post(\n      \"/auth\",\n      json!({\n        \"type\": T::req_type(),\n        \"params\": request\n      }),\n    )\n  }\n\n  #[cfg(not(feature = \"blocking\"))]\n  pub async fn read<T>(\n    &self,\n    request: T,\n  ) -> anyhow::Result<T::Response>\n  where\n    T: Serialize + KomodoReadRequest,\n    T::Response: DeserializeOwned,\n  {\n    self\n      .post(\n        \"/read\",\n        json!({\n          \"type\": T::req_type(),\n          \"params\": request\n        }),\n      )\n      .await\n  }\n\n  #[cfg(feature = \"blocking\")]\n  pub fn read<T>(&self, request: T) -> anyhow::Result<T::Response>\n  where\n    T: Serialize + KomodoReadRequest,\n    T::Response: DeserializeOwned,\n  {\n    self.post(\n      \"/read\",\n      json!({\n        \"type\": T::req_type(),\n        \"params\": request\n      }),\n    )\n  }\n\n  #[cfg(not(feature = \"blocking\"))]\n  pub async fn write<T>(\n    &self,\n    request: T,\n  ) -> anyhow::Result<T::Response>\n  where\n    T: Serialize + KomodoWriteRequest,\n    T::Response: DeserializeOwned,\n  {\n    self\n      .post(\n        \"/write\",\n        json!({\n          \"type\": T::req_type(),\n          \"params\": request\n        }),\n      )\n      .await\n  }\n\n  #[cfg(feature = \"blocking\")]\n  pub fn write<T>(&self, request: T) -> anyhow::Result<T::Response>\n  where\n    T: Serialize + KomodoWriteRequest,\n    T::Response: DeserializeOwned,\n  {\n    self.post(\n      \"/write\",\n      json!({\n        \"type\": T::req_type(),\n        \"params\": request\n      }),\n    )\n  }\n\n  #[cfg(not(feature = \"blocking\"))]\n  pub async fn execute<T>(\n    &self,\n    request: T,\n  ) -> anyhow::Result<T::Response>\n  where\n    T: Serialize + KomodoExecuteRequest,\n    T::Response: DeserializeOwned,\n  {\n    self\n      .post(\n        \"/execute\",\n        json!({\n          \"type\": T::req_type(),\n          \"params\": request\n        }),\n      )\n      .await\n  }\n\n  #[cfg(feature = \"blocking\")]\n  pub fn execute<T>(&self, request: T) -> anyhow::Result<T::Response>\n  where\n    T: Serialize + KomodoExecuteRequest,\n    T::Response: DeserializeOwned,\n  {\n    self.post(\n      \"/execute\",\n      json!({\n        \"type\": T::req_type(),\n        \"params\": request\n      }),\n    )\n  }\n\n  #[cfg(not(feature = \"blocking\"))]\n  async fn post<\n    B: Serialize + std::fmt::Debug,\n    R: DeserializeOwned,\n  >(\n    &self,\n    endpoint: &str,\n    body: B,\n  ) -> anyhow::Result<R> {\n    let req = self\n      .reqwest\n      .post(format!(\"{}{endpoint}\", self.address))\n      .header(\"x-api-key\", &self.key)\n      .header(\"x-api-secret\", &self.secret)\n      .header(\"content-type\", \"application/json\")\n      .json(&body);\n    let res =\n      req.send().await.context(\"failed to reach Komodo API\")?;\n    let status = res.status();\n    if status.is_success() {\n      match res.json().await {\n        Ok(res) => Ok(res),\n        Err(e) => Err(anyhow!(\"{e:#?}\").context(status)),\n      }\n    } else {\n      match res.text().await {\n        Ok(res) => Err(deserialize_error(res).context(status)),\n        Err(e) => Err(anyhow!(\"{e:?}\").context(status)),\n      }\n    }\n  }\n\n  #[cfg(feature = \"blocking\")]\n  fn post<B: Serialize + std::fmt::Debug, R: DeserializeOwned>(\n    &self,\n    endpoint: &str,\n    body: B,\n  ) -> anyhow::Result<R> {\n    let req = self\n      .reqwest\n      .post(format!(\"{}{endpoint}\", self.address))\n      .header(\"x-api-key\", &self.key)\n      .header(\"x-api-secret\", &self.secret)\n      .header(\"content-type\", \"application/json\")\n      .json(&body);\n    let res = req.send().context(\"failed to reach Komodo API\")?;\n    let status = res.status();\n    if status.is_success() {\n      match res.json() {\n        Ok(res) => Ok(res),\n        Err(e) => Err(anyhow!(\"{e:#?}\").context(status)),\n      }\n    } else {\n      match res.text() {\n        Ok(res) => Err(deserialize_error(res).context(status)),\n        Err(e) => Err(anyhow!(\"{e:?}\").context(status)),\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/terminal.rs",
    "content": "use futures::{Stream, StreamExt, TryStreamExt};\n\npub struct TerminalStreamResponse(pub reqwest::Response);\n\nimpl TerminalStreamResponse {\n  pub fn into_line_stream(\n    self,\n  ) -> impl Stream<Item = Result<String, tokio_util::codec::LinesCodecError>>\n  {\n    tokio_util::codec::FramedRead::new(\n      tokio_util::io::StreamReader::new(\n        self.0.bytes_stream().map_err(std::io::Error::other),\n      ),\n      tokio_util::codec::LinesCodec::new(),\n    )\n    .map(|line| line.map(|line| line + \"\\n\"))\n  }\n}\n"
  },
  {
    "path": "client/core/rs/src/ws.rs",
    "content": "use std::time::Duration;\n\nuse anyhow::Context;\nuse futures::{SinkExt, TryStreamExt};\nuse serde::{Deserialize, Serialize};\nuse serror::serialize_error;\nuse thiserror::Error;\nuse tokio::sync::broadcast;\nuse tokio_tungstenite::{connect_async, tungstenite::Message};\nuse tokio_util::sync::CancellationToken;\nuse tracing::{Instrument, debug, info, info_span, warn};\nuse typeshare::typeshare;\nuse uuid::Uuid;\n\nuse crate::{KomodoClient, entities::update::UpdateListItem};\n\n#[typeshare]\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", content = \"params\")]\npub enum WsLoginMessage {\n  Jwt { jwt: String },\n  ApiKeys { key: String, secret: String },\n}\n\nimpl WsLoginMessage {\n  pub fn from_json_str(json: &str) -> anyhow::Result<WsLoginMessage> {\n    serde_json::from_str(json)\n      .context(\"failed to parse json as WsLoginMessage\")\n  }\n\n  pub fn to_json_string(&self) -> anyhow::Result<String> {\n    serde_json::to_string(self)\n      .context(\"failed to serialize WsLoginMessage to json string\")\n  }\n}\n\n#[derive(Debug, Clone)]\npub enum UpdateWsMessage {\n  Update(UpdateListItem),\n  Error(UpdateWsError),\n  Disconnected,\n  Reconnected,\n}\n\n#[derive(Error, Debug, Clone)]\npub enum UpdateWsError {\n  #[error(\"failed to connect | {0}\")]\n  ConnectionError(String),\n  #[error(\"failed to login | {0}\")]\n  LoginError(String),\n  #[error(\"failed to recieve message | {0}\")]\n  MessageError(String),\n  #[error(\"did not recognize message | {0}\")]\n  MessageUnrecognized(String),\n}\n\nconst MAX_SHORT_RETRY_COUNT: usize = 5;\n\nimpl KomodoClient {\n  /// Subscribes to the Komodo Core update websocket,\n  /// and forwards the updates over a channel.\n  /// Handles reconnection internally.\n  ///\n  /// ```text\n  /// let (mut rx, _) = komodo.subscribe_to_updates()?;\n  /// loop {\n  ///   let update = match rx.recv().await {\n  ///     Ok(msg) => msg,\n  ///     Err(e) => {\n  ///       error!(\"🚨 recv error | {e:?}\");\n  ///       break;\n  ///     }\n  ///   };\n  ///   // Handle the update\n  ///   info!(\"Got update: {update:?}\");\n  /// }\n  /// ```\n  pub fn subscribe_to_updates(\n    &self,\n    // retry_cooldown_secs: u64,\n  ) -> anyhow::Result<(\n    broadcast::Receiver<UpdateWsMessage>,\n    CancellationToken,\n  )> {\n    let (tx, rx) = broadcast::channel(128);\n    let cancel = CancellationToken::new();\n    let cancel_clone = cancel.clone();\n    let address =\n      format!(\"{}/ws/update\", self.address.replacen(\"http\", \"ws\", 1));\n    let login_msg = WsLoginMessage::ApiKeys {\n      key: self.key.clone(),\n      secret: self.secret.clone(),\n    }\n    .to_json_string()?;\n\n    tokio::spawn(async move {\n      let master_uuid = Uuid::new_v4();\n      loop {\n        // OUTER LOOP (LONG RECONNECT)\n        if cancel.is_cancelled() {\n          break;\n        }\n\n        let outer_uuid = Uuid::new_v4();\n        let span = info_span!(\n          \"Outer Loop\",\n          master_uuid = format!(\"{master_uuid}\"),\n          outer_uuid = format!(\"{outer_uuid}\")\n        );\n\n        async {\n          debug!(\"Entering inner (connection) loop | outer uuid {outer_uuid} | master uuid {master_uuid}\");\n          let mut retry = 0;\n          loop {\n            // INNER LOOP (SHORT RECONNECT)\n            if cancel.is_cancelled() {\n              break;\n            }\n            if retry >= MAX_SHORT_RETRY_COUNT {\n              break;\n            }\n\n            let inner_uuid = Uuid::new_v4();\n            let span = info_span!(\n              \"Inner Loop\",\n              master_uuid = format!(\"{master_uuid}\"),\n              outer_uuid = format!(\"{outer_uuid}\"),\n              inner_uuid = format!(\"{inner_uuid}\")\n            );\n\n            async {\n              debug!(\"Connecting to websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\");\n\n              let mut ws =\n                match connect_async(&address).await.with_context(|| {\n                  format!(\n                    \"failed to connect to Komodo update websocket at {address}\"\n                  )\n                }) {\n                  Ok((ws, _)) => ws,\n                  Err(e) => {\n                    let _ = tx.send(UpdateWsMessage::Error(\n                      UpdateWsError::ConnectionError(serialize_error(&e)),\n                    ));\n                    warn!(\"{e:#}\");\n                    retry += 1;\n                    return;\n                  }\n                };\n\n              debug!(\"Connected to websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\");\n\n              // ==================\n              // SEND LOGIN MSG\n              // ==================\n              let login_send_res = ws\n                .send(Message::text(&login_msg))\n                .await\n                .context(\"failed to send login message\");\n\n              if let Err(e) = login_send_res {\n                let _ = tx.send(UpdateWsMessage::Error(\n                  UpdateWsError::LoginError(serialize_error(&e)),\n                ));\n                warn!(\"breaking inner loop | {e:#} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\");\n                retry += 1;\n                return;\n              }\n\n              // ==================\n              // HANDLE LOGIN RES\n              // ==================\n              match ws.try_next().await {\n                Ok(Some(Message::Text(msg))) => {\n                  if msg != \"LOGGED_IN\" {\n                    let _ = tx.send(UpdateWsMessage::Error(\n                      UpdateWsError::LoginError(msg.to_string()),\n                    ));\n                    let _ = ws.close(None).await;\n                    warn!(\"breaking inner loop | got msg {msg} instead of 'LOGGED_IN' | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\");\n                    retry += 1;\n                    return;\n                  }\n                }\n                Ok(Some(msg)) => {\n                  let _ = tx.send(UpdateWsMessage::Error(\n                    UpdateWsError::LoginError(format!(\"{msg:#?}\")),\n                  ));\n                  let _ = ws.close(None).await;\n                  warn!(\"breaking inner loop | got msg {msg} instead of Message::Text 'LOGGED_IN' | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\");\n                  retry += 1;\n                  return;\n                }\n                Ok(None) => {\n                  let _ = tx.send(UpdateWsMessage::Error(\n                    UpdateWsError::LoginError(String::from(\n                      \"got None message after login message\",\n                    )),\n                  ));\n                  let _ = ws.close(None).await;\n                  warn!(\"breaking inner loop | got None instead of 'LOGGED_IN' | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\");\n                  retry += 1;\n                  return;\n                }\n                Err(e) => {\n                  let _ = tx.send(UpdateWsMessage::Error(\n                    UpdateWsError::LoginError(format!(\n                      \"failed to recieve message | {e:#?}\"\n                    )),\n                  ));\n                  let _ = ws.close(None).await;\n                  warn!(\"breaking inner loop | got error msg | {e:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\");\n                  retry += 1;\n                  return;\n                }\n              }\n\n              let _ = tx.send(UpdateWsMessage::Reconnected);\n\n              info!(\"Logged into websocket | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\");\n\n              // If we get to this point (connected / logged in) reset the short retry counter\n              retry = 0;\n\n              // ==================\n              // HANLDE MSGS\n              // ==================\n              loop {\n                match ws\n                  .try_next()\n                  .await\n                  .context(\"failed to recieve message\")\n                {\n                  Ok(Some(Message::Text(msg))) => {\n                    match serde_json::from_str::<UpdateListItem>(&msg) {\n                      Ok(msg) => {\n                        debug!(\n                          \"got recognized message: {msg:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\"\n                        );\n                        let _ = tx.send(UpdateWsMessage::Update(msg));\n                      }\n                      Err(_) => {\n                        warn!(\n                          \"got unrecognized message: {msg:?} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\"\n                        );\n                        let _ = tx.send(UpdateWsMessage::Error(\n                          UpdateWsError::MessageUnrecognized(msg.to_string()),\n                        ));\n                      }\n                    }\n                  }\n                  Ok(Some(Message::Close(_))) => {\n                    let _ = tx.send(UpdateWsMessage::Disconnected);\n                    let _ = ws.close(None).await;\n                    warn!(\n                      \"breaking inner loop | got close message | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\"\n                    );\n                    break;\n                  }\n                  Err(e) => {\n                    let _ = tx.send(UpdateWsMessage::Error(\n                      UpdateWsError::MessageError(serialize_error(&e)),\n                    ));\n                    let _ = tx.send(UpdateWsMessage::Disconnected);\n                    let _ = ws.close(None).await;\n                    warn!(\n                      \"breaking inner loop | got error message | {e:#} | inner uuid {inner_uuid} | outer uuid {outer_uuid} | master uuid {master_uuid}\"\n                    );\n                    break;\n                  }\n                  Ok(_) => {\n                    // ignore\n                  }\n                }\n              }\n            }\n              .instrument(span)\n              .await;\n          }\n        }.instrument(span).await;\n\n        tokio::time::sleep(Duration::from_secs(3)).await;\n      }\n    });\n\n    Ok((rx, cancel_clone))\n  }\n}\n"
  },
  {
    "path": "client/core/ts/README.md",
    "content": "# Komodo\n\n_A system to build and deploy software across many servers_. [https://komo.do](https://komo.do)\n\n```sh\nnpm install komodo_client\n```\n\nor\n\n```sh\nyarn add komodo_client\n```\n\n```ts\nimport { KomodoClient, Types } from \"komodo_client\";\n\nconst komodo = KomodoClient(\"https://demo.komo.do\", {\n  type: \"api-key\",\n  params: {\n    key: \"your_key\",\n    secret: \"your secret\",\n  },\n});\n\n// Inferred as Types.StackListItem[]\nconst stacks = await komodo.read(\"ListStacks\", {});\n\n// Inferred as Types.Stack\nconst stack = await komodo.read(\"GetStack\", {\n  stack: stacks[0].name,\n});\n```\n"
  },
  {
    "path": "client/core/ts/generate_types.mjs",
    "content": "import { exec } from \"child_process\";\nimport { readFileSync, writeFileSync } from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconsole.log(\"generating typescript types...\");\n\nconst gen_command =\n  \"RUST_BACKTRACE=1 typeshare . --lang=typescript --output-file=./client/core/ts/src/types.ts\";\n\nexec(gen_command, (error, _stdout, _stderr) => {\n  if (error) {\n    console.error(error);\n    return;\n  }\n  console.log(\"generated types using typeshare\");\n  fix_types();\n  console.log(\"finished.\");\n});\n\nfunction fix_types() {\n  const types_path = __dirname + \"/src/types.ts\";\n  const contents = readFileSync(types_path);\n  const fixed = contents\n    .toString()\n    // Replace Variants\n    .replaceAll(\"ResourceTargetVariant\", 'ResourceTarget[\"type\"]')\n    .replaceAll(\"AlerterEndpointVariant\", 'AlerterEndpoint[\"type\"]')\n    .replaceAll(\"AlertDataVariant\", 'AlertData[\"type\"]')\n    .replaceAll(\"ServerTemplateConfigVariant\", 'ServerTemplateConfig[\"type\"]')\n    // Add '| string' to env vars\n    .replaceAll(\"EnvironmentVar[]\", \"EnvironmentVar[] | string\")\n    .replaceAll(\"IndexSet\", \"Array\")\n    .replaceAll(\n      \": PermissionLevelAndSpecifics\",\n      \": PermissionLevelAndSpecifics | PermissionLevel\"\n    )\n    .replaceAll(\n      \", PermissionLevelAndSpecifics\",\n      \", PermissionLevelAndSpecifics | PermissionLevel\"\n    )\n    .replaceAll(\"IndexMap\", \"Record\");\n  writeFileSync(types_path, fixed);\n}\n"
  },
  {
    "path": "client/core/ts/package.json",
    "content": "{\n  \"name\": \"komodo_client\",\n  \"version\": \"1.19.5\",\n  \"description\": \"Komodo client package\",\n  \"homepage\": \"https://komo.do\",\n  \"main\": \"dist/lib.js\",\n  \"type\": \"module\",\n  \"license\": \"GPL-3.0\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc\"\n  },\n  \"dependencies\": {},\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"packageManager\": \"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e\"\n}\n"
  },
  {
    "path": "client/core/ts/runfile.toml",
    "content": "[publish-ts-client]\ndescription = \"publish the typescript client to npm\"\ncmd = \"npm publish\""
  },
  {
    "path": "client/core/ts/src/lib.ts",
    "content": "import {\n  AuthResponses,\n  ExecuteResponses,\n  ReadResponses,\n  UserResponses,\n  WriteResponses,\n} from \"./responses.js\";\nimport {\n  terminal_methods,\n  ConnectExecQuery,\n  ExecuteExecBody,\n  TerminalCallbacks,\n} from \"./terminal.js\";\nimport {\n  AuthRequest,\n  BatchExecutionResponse,\n  ConnectTerminalQuery,\n  ExecuteRequest,\n  ExecuteTerminalBody,\n  ReadRequest,\n  Update,\n  UpdateListItem,\n  UpdateStatus,\n  UserRequest,\n  WriteRequest,\n  WsLoginMessage,\n} from \"./types.js\";\n\nexport * as Types from \"./types.js\";\n\nexport type { ConnectExecQuery, ExecuteExecBody, TerminalCallbacks };\n\nexport type InitOptions =\n  | { type: \"jwt\"; params: { jwt: string } }\n  | { type: \"api-key\"; params: { key: string; secret: string } };\n\nexport class CancelToken {\n  cancelled: boolean;\n  constructor() {\n    this.cancelled = false;\n  }\n  cancel() {\n    this.cancelled = true;\n  }\n}\n\nexport type ClientState = {\n  jwt: string | undefined;\n  key: string | undefined;\n  secret: string | undefined;\n};\n\n/** Initialize a new client for Komodo */\nexport function KomodoClient(url: string, options: InitOptions) {\n  const state: ClientState = {\n    jwt: options.type === \"jwt\" ? options.params.jwt : undefined,\n    key: options.type === \"api-key\" ? options.params.key : undefined,\n    secret: options.type === \"api-key\" ? options.params.secret : undefined,\n  };\n\n  const request = <Params, Res>(\n    path: \"/auth\" | \"/user\" | \"/read\" | \"/execute\" | \"/write\",\n    type: string,\n    params: Params\n  ): Promise<Res> =>\n    new Promise(async (res, rej) => {\n      try {\n        let response = await fetch(`${url}${path}/${type}`, {\n          method: \"POST\",\n          body: JSON.stringify(params),\n          headers: {\n            ...(state.jwt\n              ? {\n                  authorization: state.jwt,\n                }\n              : state.key && state.secret\n              ? {\n                  \"x-api-key\": state.key,\n                  \"x-api-secret\": state.secret,\n                }\n              : {}),\n            \"content-type\": \"application/json\",\n          },\n        });\n        if (response.status === 200) {\n          const body: Res = await response.json();\n          res(body);\n        } else {\n          try {\n            const result = await response.json();\n            rej({ status: response.status, result });\n          } catch (error) {\n            rej({\n              status: response.status,\n              result: {\n                error: \"Failed to get response body\",\n                trace: [JSON.stringify(error)],\n              },\n              error,\n            });\n          }\n        }\n      } catch (error) {\n        rej({\n          status: 1,\n          result: {\n            error: \"Request failed with error\",\n            trace: [JSON.stringify(error)],\n          },\n          error,\n        });\n      }\n    });\n\n  const auth = async <\n    T extends AuthRequest[\"type\"],\n    Req extends Extract<AuthRequest, { type: T }>\n  >(\n    type: T,\n    params: Req[\"params\"]\n  ) =>\n    await request<Req[\"params\"], AuthResponses[Req[\"type\"]]>(\n      \"/auth\",\n      type,\n      params\n    );\n\n  const user = async <\n    T extends UserRequest[\"type\"],\n    Req extends Extract<UserRequest, { type: T }>\n  >(\n    type: T,\n    params: Req[\"params\"]\n  ) =>\n    await request<Req[\"params\"], UserResponses[Req[\"type\"]]>(\n      \"/user\",\n      type,\n      params\n    );\n\n  const read = async <\n    T extends ReadRequest[\"type\"],\n    Req extends Extract<ReadRequest, { type: T }>\n  >(\n    type: T,\n    params: Req[\"params\"]\n  ) =>\n    await request<Req[\"params\"], ReadResponses[Req[\"type\"]]>(\n      \"/read\",\n      type,\n      params\n    );\n\n  const write = async <\n    T extends WriteRequest[\"type\"],\n    Req extends Extract<WriteRequest, { type: T }>\n  >(\n    type: T,\n    params: Req[\"params\"]\n  ) =>\n    await request<Req[\"params\"], WriteResponses[Req[\"type\"]]>(\n      \"/write\",\n      type,\n      params\n    );\n\n  const execute = async <\n    T extends ExecuteRequest[\"type\"],\n    Req extends Extract<ExecuteRequest, { type: T }>\n  >(\n    type: T,\n    params: Req[\"params\"]\n  ) =>\n    await request<Req[\"params\"], ExecuteResponses[Req[\"type\"]]>(\n      \"/execute\",\n      type,\n      params\n    );\n\n  const execute_and_poll = async <\n    T extends ExecuteRequest[\"type\"],\n    Req extends Extract<ExecuteRequest, { type: T }>\n  >(\n    type: T,\n    params: Req[\"params\"]\n  ) => {\n    const res = await execute(type, params);\n    // Check if its a batch of updates or a single update;\n    if (Array.isArray(res)) {\n      const batch = res as any as BatchExecutionResponse;\n      return await Promise.all(\n        batch.map(async (item) => {\n          if (item.status === \"Err\") {\n            return item;\n          }\n          return await poll_update_until_complete(item.data._id?.$oid!);\n        })\n      );\n    } else {\n      // it is a single update\n      const update = res as any as Update;\n\n      if (update.status === UpdateStatus.Complete || !update._id?.$oid) {\n        return update;\n      }\n\n      return await poll_update_until_complete(update._id?.$oid!);\n    }\n  };\n\n  const poll_update_until_complete = async (update_id: string) => {\n    while (true) {\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n      const update = await read(\"GetUpdate\", { id: update_id });\n      if (update.status === UpdateStatus.Complete) {\n        return update;\n      }\n    }\n  };\n\n  const core_version = () => read(\"GetVersion\", {}).then((res) => res.version);\n\n  const get_update_websocket = ({\n    on_update,\n    on_login,\n    on_open,\n    on_close,\n  }: {\n    on_update: (update: UpdateListItem) => void;\n    on_login?: () => void;\n    on_open?: () => void;\n    on_close?: () => void;\n  }) => {\n    const ws = new WebSocket(url.replace(\"http\", \"ws\") + \"/ws/update\");\n\n    // Handle login on websocket open\n    ws.addEventListener(\"open\", () => {\n      on_open?.();\n      const login_msg: WsLoginMessage =\n        options.type === \"jwt\"\n          ? {\n              type: \"Jwt\",\n              params: {\n                jwt: options.params.jwt,\n              },\n            }\n          : {\n              type: \"ApiKeys\",\n              params: {\n                key: options.params.key,\n                secret: options.params.secret,\n              },\n            };\n      ws.send(JSON.stringify(login_msg));\n    });\n\n    ws.addEventListener(\"message\", ({ data }: MessageEvent) => {\n      if (data == \"LOGGED_IN\") return on_login?.();\n      on_update(JSON.parse(data));\n    });\n\n    if (on_close) {\n      ws.addEventListener(\"close\", on_close);\n    }\n\n    return ws;\n  };\n\n  const subscribe_to_update_websocket = async ({\n    on_update,\n    on_open,\n    on_login,\n    on_close,\n    retry = true,\n    retry_timeout_ms = 5_000,\n    cancel = new CancelToken(),\n    on_cancel,\n  }: {\n    on_update: (update: UpdateListItem) => void;\n    on_login?: () => void;\n    on_open?: () => void;\n    on_close?: () => void;\n    retry?: boolean;\n    retry_timeout_ms?: number;\n    cancel?: CancelToken;\n    on_cancel?: () => void;\n  }) => {\n    while (true) {\n      if (cancel.cancelled) {\n        on_cancel?.();\n        return;\n      }\n\n      try {\n        const ws = get_update_websocket({\n          on_open,\n          on_login,\n          on_update,\n          on_close,\n        });\n\n        // This while loop will end when the socket is closed\n        while (\n          ws.readyState !== WebSocket.CLOSING &&\n          ws.readyState !== WebSocket.CLOSED\n        ) {\n          if (cancel.cancelled) ws.close();\n          // Sleep for a bit before checking for websocket closed\n          await new Promise((resolve) => setTimeout(resolve, 500));\n        }\n\n        if (retry) {\n          // Sleep for a bit before retrying connection to avoid spam.\n          await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));\n        } else {\n          return;\n        }\n      } catch (error) {\n        console.error(error);\n        if (retry) {\n          // Sleep for a bit before retrying, maybe Komodo Core is down temporarily.\n          await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));\n        } else {\n          return;\n        }\n      }\n    }\n  };\n\n  const {\n    connect_terminal,\n    execute_terminal,\n    execute_terminal_stream,\n    connect_exec,\n    connect_container_exec,\n    execute_container_exec,\n    execute_container_exec_stream,\n    connect_deployment_exec,\n    execute_deployment_exec,\n    execute_deployment_exec_stream,\n    connect_stack_exec,\n    execute_stack_exec,\n    execute_stack_exec_stream,\n  } = terminal_methods(url, state);\n\n  return {\n    /**\n     * Call the `/auth` api.\n     *\n     * ```\n     * const login_options = await komodo.auth(\"GetLoginOptions\", {});\n     * ```\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/auth/index.html\n     */\n    auth,\n    /**\n     * Call the `/user` api.\n     *\n     * ```\n     * const { key, secret } = await komodo.user(\"CreateApiKey\", {\n     *   name: \"my-api-key\"\n     * });\n     * ```\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/user/index.html\n     */\n    user,\n    /**\n     * Call the `/read` api.\n     *\n     * ```\n     * const stack = await komodo.read(\"GetStack\", {\n     *   stack: \"my-stack\"\n     * });\n     * ```\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/read/index.html\n     */\n    read,\n    /**\n     * Call the `/write` api.\n     *\n     * ```\n     * const build = await komodo.write(\"UpdateBuild\", {\n     *   id: \"my-build\",\n     *   config: {\n     *     version: \"1.0.4\"\n     *   }\n     * });\n     * ```\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/write/index.html\n     */\n    write,\n    /**\n     * Call the `/execute` api.\n     *\n     * ```\n     * const update = await komodo.execute(\"DeployStack\", {\n     *   stack: \"my-stack\"\n     * });\n     * ```\n     *\n     * NOTE. These calls return immediately when the update is created, NOT when the execution task finishes.\n     * To have the call only return when the task finishes, use [execute_and_poll_until_complete].\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html\n     */\n    execute,\n    /**\n     * Call the `/execute` api, and poll the update until the task has completed.\n     *\n     * ```\n     * const update = await komodo.execute_and_poll(\"DeployStack\", {\n     *   stack: \"my-stack\"\n     * });\n     * ```\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html\n     */\n    execute_and_poll,\n    /**\n     * Poll an Update (returned by the `execute` calls) until the `status` is `Complete`.\n     * https://docs.rs/komodo_client/latest/komodo_client/entities/update/struct.Update.html#structfield.status.\n     */\n    poll_update_until_complete,\n    /** Returns the version of Komodo Core the client is calling to. */\n    core_version,\n    /**\n     * Connects to update websocket, performs login and attaches handlers,\n     * and returns the WebSocket handle.\n     */\n    get_update_websocket,\n    /**\n     * Subscribes to the update websocket with automatic reconnect loop.\n     *\n     * Note. Awaiting this method will never finish.\n     */\n    subscribe_to_update_websocket,\n    /**\n     * Subscribes to terminal io over websocket message,\n     * for use with xtermjs.\n     */\n    connect_terminal,\n    /**\n     * Executes a command on a given Server / terminal,\n     * and gives a callback to handle the output as it comes in.\n     *\n     * ```ts\n     * await komodo.execute_terminal(\n     *   {\n     *     server: \"my-server\",\n     *     terminal: \"name\",\n     *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     *   },\n     *   {\n     *     onLine: (line) => console.log(line),\n     *     onFinish: (code) => console.log(\"Finished:\", code),\n     *   }\n     * );\n     * ```\n     */\n    execute_terminal,\n    /**\n     * Executes a command on a given Server / terminal,\n     * and returns a stream to process the output as it comes in.\n     *\n     * Note. The final line of the stream will usually be\n     * `__KOMODO_EXIT_CODE__:0`. The number\n     * is the exit code of the command.\n     *\n     * If this line is NOT present, it means the stream\n     * was terminated early, ie like running `exit`.\n     *\n     * ```ts\n     * const stream = await komodo.execute_terminal_stream({\n     *   server: \"my-server\",\n     *   terminal: \"name\",\n     *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     * });\n     *\n     * for await (const line of stream) {\n     *   console.log(line);\n     * }\n     * ```\n     */\n    execute_terminal_stream,\n    /**\n     * Subscribes to container exec io over websocket message,\n     * for use with xtermjs. Can connect to container on a Server,\n     * or associated with a Deployment or Stack.\n     * Terminal permission on connecting resource required.\n     */\n    connect_exec,\n    /**\n     * Subscribes to container exec io over websocket message,\n     * for use with xtermjs. Can connect to Container on a Server.\n     * Server Terminal permission required.\n     */\n    connect_container_exec,\n    /**\n     * Executes a command on a given container,\n     * and gives a callback to handle the output as it comes in.\n     *\n     * ```ts\n     * await komodo.execute_container_exec(\n     *   {\n     *     server: \"my-server\",\n     *     container: \"name\",\n     *     shell: \"bash\",\n     *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     *   },\n     *   {\n     *     onLine: (line) => console.log(line),\n     *     onFinish: (code) => console.log(\"Finished:\", code),\n     *   }\n     * );\n     * ```\n     */\n    execute_container_exec,\n    /**\n     * Executes a command on a given container,\n     * and returns a stream to process the output as it comes in.\n     *\n     * Note. The final line of the stream will usually be\n     * `__KOMODO_EXIT_CODE__:0`. The number\n     * is the exit code of the command.\n     *\n     * If this line is NOT present, it means the stream\n     * was terminated early, ie like running `exit`.\n     *\n     * ```ts\n     * const stream = await komodo.execute_container_exec_stream({\n     *   server: \"my-server\",\n     *   container: \"name\",\n     *   shell: \"bash\",\n     *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     * });\n     *\n     * for await (const line of stream) {\n     *   console.log(line);\n     * }\n     * ```\n     */\n    execute_container_exec_stream,\n    /**\n     * Subscribes to deployment container exec io over websocket message,\n     * for use with xtermjs. Can connect to Deployment container.\n     * Deployment Terminal permission required.\n     */\n    connect_deployment_exec,\n    /**\n     * Executes a command on a given deployment container,\n     * and gives a callback to handle the output as it comes in.\n     *\n     * ```ts\n     * await komodo.execute_deployment_exec(\n     *   {\n     *     deployment: \"my-deployment\",\n     *     shell: \"bash\",\n     *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     *   },\n     *   {\n     *     onLine: (line) => console.log(line),\n     *     onFinish: (code) => console.log(\"Finished:\", code),\n     *   }\n     * );\n     * ```\n     */\n    execute_deployment_exec,\n    /**\n     * Executes a command on a given deployment container,\n     * and returns a stream to process the output as it comes in.\n     *\n     * Note. The final line of the stream will usually be\n     * `__KOMODO_EXIT_CODE__:0`. The number\n     * is the exit code of the command.\n     *\n     * If this line is NOT present, it means the stream\n     * was terminated early, ie like running `exit`.\n     *\n     * ```ts\n     * const stream = await komodo.execute_deployment_exec_stream({\n     *   deployment: \"my-deployment\",\n     *   shell: \"bash\",\n     *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     * });\n     *\n     * for await (const line of stream) {\n     *   console.log(line);\n     * }\n     * ```\n     */\n    execute_deployment_exec_stream,\n    /**\n     * Subscribes to container exec io over websocket message,\n     * for use with xtermjs. Can connect to Stack service container.\n     * Stack Terminal permission required.\n     */\n    connect_stack_exec,\n    /**\n     * Executes a command on a given stack service container,\n     * and gives a callback to handle the output as it comes in.\n     *\n     * ```ts\n     * await komodo.execute_stack_exec(\n     *   {\n     *     stack: \"my-stack\",\n     *     service: \"database\"\n     *     shell: \"bash\",\n     *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     *   },\n     *   {\n     *     onLine: (line) => console.log(line),\n     *     onFinish: (code) => console.log(\"Finished:\", code),\n     *   }\n     * );\n     * ```\n     */\n    execute_stack_exec,\n    /**\n     * Executes a command on a given stack service container,\n     * and returns a stream to process the output as it comes in.\n     *\n     * Note. The final line of the stream will usually be\n     * `__KOMODO_EXIT_CODE__:0`. The number\n     * is the exit code of the command.\n     *\n     * If this line is NOT present, it means the stream\n     * was terminated early, ie like running `exit`.\n     *\n     * ```ts\n     * const stream = await komodo.execute_stack_exec_stream({\n     *   stack: \"my-stack\",\n     *   service: \"service1\",\n     *   shell: \"bash\",\n     *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     * });\n     *\n     * for await (const line of stream) {\n     *   console.log(line);\n     * }\n     * ```\n     */\n    execute_stack_exec_stream,\n  };\n}\n"
  },
  {
    "path": "client/core/ts/src/responses.ts",
    "content": "import * as Types from \"./types.js\";\n\nexport type AuthResponses = {\n  GetLoginOptions: Types.GetLoginOptionsResponse;\n  SignUpLocalUser: Types.SignUpLocalUserResponse;\n  LoginLocalUser: Types.LoginLocalUserResponse;\n  ExchangeForJwt: Types.ExchangeForJwtResponse;\n  GetUser: Types.GetUserResponse;\n};\n\nexport type UserResponses = {\n  PushRecentlyViewed: Types.PushRecentlyViewedResponse;\n  SetLastSeenUpdate: Types.SetLastSeenUpdateResponse;\n  CreateApiKey: Types.CreateApiKeyResponse;\n  DeleteApiKey: Types.DeleteApiKeyResponse;\n};\n\nexport type ReadResponses = {\n  GetVersion: Types.GetVersionResponse;\n  GetCoreInfo: Types.GetCoreInfoResponse;\n  ListSecrets: Types.ListSecretsResponse;\n  ListGitProvidersFromConfig: Types.ListGitProvidersFromConfigResponse;\n  ListDockerRegistriesFromConfig: Types.ListDockerRegistriesFromConfigResponse;\n\n  // ==== USER ====\n  GetUsername: Types.GetUsernameResponse;\n  GetPermission: Types.GetPermissionResponse;\n  FindUser: Types.FindUserResponse;\n  ListUsers: Types.ListUsersResponse;\n  ListApiKeys: Types.ListApiKeysResponse;\n  ListApiKeysForServiceUser: Types.ListApiKeysForServiceUserResponse;\n  ListPermissions: Types.ListPermissionsResponse;\n  ListUserTargetPermissions: Types.ListUserTargetPermissionsResponse;\n\n  // ==== USER GROUP ====\n  GetUserGroup: Types.GetUserGroupResponse;\n  ListUserGroups: Types.ListUserGroupsResponse;\n\n  // ==== PROCEDURE ====\n  GetProceduresSummary: Types.GetProceduresSummaryResponse;\n  GetProcedure: Types.GetProcedureResponse;\n  GetProcedureActionState: Types.GetProcedureActionStateResponse;\n  ListProcedures: Types.ListProceduresResponse;\n  ListFullProcedures: Types.ListFullProceduresResponse;\n\n  // ==== ACTION ====\n  GetActionsSummary: Types.GetActionsSummaryResponse;\n  GetAction: Types.GetActionResponse;\n  GetActionActionState: Types.GetActionActionStateResponse;\n  ListActions: Types.ListActionsResponse;\n  ListFullActions: Types.ListFullActionsResponse;\n\n  // ==== SCHEDULE ====\n  ListSchedules: Types.ListSchedulesResponse;\n\n  // ==== SERVER ====\n  GetServersSummary: Types.GetServersSummaryResponse;\n  GetServer: Types.GetServerResponse;\n  GetServerState: Types.GetServerStateResponse;\n  GetPeripheryVersion: Types.GetPeripheryVersionResponse;\n  GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse;\n  ListDockerContainers: Types.ListDockerContainersResponse;\n  ListAllDockerContainers: Types.ListAllDockerContainersResponse;\n  InspectDockerContainer: Types.InspectDockerContainerResponse;\n  GetResourceMatchingContainer: Types.GetResourceMatchingContainerResponse;\n  GetContainerLog: Types.GetContainerLogResponse;\n  SearchContainerLog: Types.SearchContainerLogResponse;\n  ListDockerNetworks: Types.ListDockerNetworksResponse;\n  InspectDockerNetwork: Types.InspectDockerNetworkResponse;\n  ListDockerImages: Types.ListDockerImagesResponse;\n  InspectDockerImage: Types.InspectDockerImageResponse;\n  ListDockerImageHistory: Types.ListDockerImageHistoryResponse;\n  ListDockerVolumes: Types.ListDockerVolumesResponse;\n  InspectDockerVolume: Types.InspectDockerVolumeResponse;\n  ListComposeProjects: Types.ListComposeProjectsResponse;\n  GetServerActionState: Types.GetServerActionStateResponse;\n  GetHistoricalServerStats: Types.GetHistoricalServerStatsResponse;\n  ListServers: Types.ListServersResponse;\n  ListFullServers: Types.ListFullServersResponse;\n  ListTerminals: Types.ListTerminalsResponse;\n\n  // ==== STACK ====\n  GetStacksSummary: Types.GetStacksSummaryResponse;\n  GetStack: Types.GetStackResponse;\n  GetStackActionState: Types.GetStackActionStateResponse;\n  GetStackWebhooksEnabled: Types.GetStackWebhooksEnabledResponse;\n  GetStackLog: Types.GetStackLogResponse;\n  SearchStackLog: Types.SearchStackLogResponse;\n  InspectStackContainer: Types.InspectStackContainerResponse;\n  ListStacks: Types.ListStacksResponse;\n  ListFullStacks: Types.ListFullStacksResponse;\n  ListStackServices: Types.ListStackServicesResponse;\n  ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse;\n  ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse;\n\n  // ==== DEPLOYMENT ====\n  GetDeploymentsSummary: Types.GetDeploymentsSummaryResponse;\n  GetDeployment: Types.GetDeploymentResponse;\n  GetDeploymentContainer: Types.GetDeploymentContainerResponse;\n  GetDeploymentActionState: Types.GetDeploymentActionStateResponse;\n  GetDeploymentStats: Types.GetDeploymentStatsResponse;\n  GetDeploymentLog: Types.GetDeploymentLogResponse;\n  SearchDeploymentLog: Types.SearchDeploymentLogResponse;\n  InspectDeploymentContainer: Types.InspectDeploymentContainerResponse;\n  ListDeployments: Types.ListDeploymentsResponse;\n  ListFullDeployments: Types.ListFullDeploymentsResponse;\n  ListCommonDeploymentExtraArgs: Types.ListCommonDeploymentExtraArgsResponse;\n\n  // ==== BUILD ====\n  GetBuildsSummary: Types.GetBuildsSummaryResponse;\n  GetBuild: Types.GetBuildResponse;\n  GetBuildActionState: Types.GetBuildActionStateResponse;\n  GetBuildMonthlyStats: Types.GetBuildMonthlyStatsResponse;\n  GetBuildWebhookEnabled: Types.GetBuildWebhookEnabledResponse;\n  ListBuilds: Types.ListBuildsResponse;\n  ListFullBuilds: Types.ListFullBuildsResponse;\n  ListBuildVersions: Types.ListBuildVersionsResponse;\n  ListCommonBuildExtraArgs: Types.ListCommonBuildExtraArgsResponse;\n\n  // ==== REPO ====\n  GetReposSummary: Types.GetReposSummaryResponse;\n  GetRepo: Types.GetRepoResponse;\n  GetRepoActionState: Types.GetRepoActionStateResponse;\n  GetRepoWebhooksEnabled: Types.GetRepoWebhooksEnabledResponse;\n  ListRepos: Types.ListReposResponse;\n  ListFullRepos: Types.ListFullReposResponse;\n\n  // ==== SYNC ====\n  GetResourceSyncsSummary: Types.GetResourceSyncsSummaryResponse;\n  GetResourceSync: Types.GetResourceSyncResponse;\n  GetResourceSyncActionState: Types.GetResourceSyncActionStateResponse;\n  GetSyncWebhooksEnabled: Types.GetSyncWebhooksEnabledResponse;\n  ListResourceSyncs: Types.ListResourceSyncsResponse;\n  ListFullResourceSyncs: Types.ListFullResourceSyncsResponse;\n\n  // ==== BUILDER ====\n  GetBuildersSummary: Types.GetBuildersSummaryResponse;\n  GetBuilder: Types.GetBuilderResponse;\n  ListBuilders: Types.ListBuildersResponse;\n  ListFullBuilders: Types.ListFullBuildersResponse;\n\n  // ==== ALERTER ====\n  GetAlertersSummary: Types.GetAlertersSummaryResponse;\n  GetAlerter: Types.GetAlerterResponse;\n  ListAlerters: Types.ListAlertersResponse;\n  ListFullAlerters: Types.ListFullAlertersResponse;\n\n  // ==== TOML ====\n  ExportAllResourcesToToml: Types.ExportAllResourcesToTomlResponse;\n  ExportResourcesToToml: Types.ExportResourcesToTomlResponse;\n\n  // ==== TAG ====\n  GetTag: Types.GetTagResponse;\n  ListTags: Types.ListTagsResponse;\n\n  // ==== UPDATE ====\n  GetUpdate: Types.GetUpdateResponse;\n  ListUpdates: Types.ListUpdatesResponse;\n\n  // ==== ALERT ====\n  ListAlerts: Types.ListAlertsResponse;\n  GetAlert: Types.GetAlertResponse;\n\n  // ==== SERVER STATS ====\n  GetSystemInformation: Types.GetSystemInformationResponse;\n  GetSystemStats: Types.GetSystemStatsResponse;\n  ListSystemProcesses: Types.ListSystemProcessesResponse;\n\n  // ==== VARIABLE ====\n  GetVariable: Types.GetVariableResponse;\n  ListVariables: Types.ListVariablesResponse;\n\n  // ==== PROVIDER ====\n  GetGitProviderAccount: Types.GetGitProviderAccountResponse;\n  ListGitProviderAccounts: Types.ListGitProviderAccountsResponse;\n  GetDockerRegistryAccount: Types.GetDockerRegistryAccountResponse;\n  ListDockerRegistryAccounts: Types.ListDockerRegistryAccountsResponse;\n};\n\nexport type WriteResponses = {\n  // ==== USER ====\n  CreateLocalUser: Types.CreateLocalUserResponse;\n  UpdateUserUsername: Types.UpdateUserUsernameResponse;\n  UpdateUserPassword: Types.UpdateUserPasswordResponse;\n  DeleteUser: Types.DeleteUserResponse;\n\n  // ==== SERVICE USER ====\n  CreateServiceUser: Types.CreateServiceUserResponse;\n  UpdateServiceUserDescription: Types.UpdateServiceUserDescriptionResponse;\n  CreateApiKeyForServiceUser: Types.CreateApiKeyForServiceUserResponse;\n  DeleteApiKeyForServiceUser: Types.DeleteApiKeyForServiceUserResponse;\n\n  // ==== USER GROUP ====\n  CreateUserGroup: Types.UserGroup;\n  RenameUserGroup: Types.UserGroup;\n  DeleteUserGroup: Types.UserGroup;\n  AddUserToUserGroup: Types.UserGroup;\n  RemoveUserFromUserGroup: Types.UserGroup;\n  SetUsersInUserGroup: Types.UserGroup;\n  SetEveryoneUserGroup: Types.UserGroup;\n\n  // ==== PERMISSIONS ====\n  UpdateUserAdmin: Types.UpdateUserAdminResponse;\n  UpdateUserBasePermissions: Types.UpdateUserBasePermissionsResponse;\n  UpdatePermissionOnResourceType: Types.UpdatePermissionOnResourceTypeResponse;\n  UpdatePermissionOnTarget: Types.UpdatePermissionOnTargetResponse;\n\n  // ==== RESOURCE ====\n  UpdateResourceMeta: Types.UpdateResourceMetaResponse;\n\n  // ==== SERVER ====\n  CreateServer: Types.Server;\n  CopyServer: Types.Server;\n  DeleteServer: Types.Server;\n  UpdateServer: Types.Server;\n  RenameServer: Types.Update;\n  CreateNetwork: Types.Update;\n  CreateTerminal: Types.NoData;\n  DeleteTerminal: Types.NoData;\n  DeleteAllTerminals: Types.NoData;\n\n  // ==== STACK ====\n  CreateStack: Types.Stack;\n  CopyStack: Types.Stack;\n  DeleteStack: Types.Stack;\n  UpdateStack: Types.Stack;\n  RenameStack: Types.Update;\n  WriteStackFileContents: Types.Update;\n  RefreshStackCache: Types.NoData;\n  CreateStackWebhook: Types.CreateStackWebhookResponse;\n  DeleteStackWebhook: Types.DeleteStackWebhookResponse;\n\n  // ==== DEPLOYMENT ====\n  CreateDeployment: Types.Deployment;\n  CopyDeployment: Types.Deployment;\n  CreateDeploymentFromContainer: Types.Deployment;\n  DeleteDeployment: Types.Deployment;\n  UpdateDeployment: Types.Deployment;\n  RenameDeployment: Types.Update;\n\n  // ==== BUILD ====\n  CreateBuild: Types.Build;\n  CopyBuild: Types.Build;\n  DeleteBuild: Types.Build;\n  UpdateBuild: Types.Build;\n  RenameBuild: Types.Update;\n  WriteBuildFileContents: Types.Update;\n  RefreshBuildCache: Types.NoData;\n  CreateBuildWebhook: Types.CreateBuildWebhookResponse;\n  DeleteBuildWebhook: Types.DeleteBuildWebhookResponse;\n\n  // ==== BUILDER ====\n  CreateBuilder: Types.Builder;\n  CopyBuilder: Types.Builder;\n  DeleteBuilder: Types.Builder;\n  UpdateBuilder: Types.Builder;\n  RenameBuilder: Types.Update;\n\n  // ==== REPO ====\n  CreateRepo: Types.Repo;\n  CopyRepo: Types.Repo;\n  DeleteRepo: Types.Repo;\n  UpdateRepo: Types.Repo;\n  RenameRepo: Types.Update;\n  RefreshRepoCache: Types.NoData;\n  CreateRepoWebhook: Types.CreateRepoWebhookResponse;\n  DeleteRepoWebhook: Types.DeleteRepoWebhookResponse;\n\n  // ==== ALERTER ====\n  CreateAlerter: Types.Alerter;\n  CopyAlerter: Types.Alerter;\n  DeleteAlerter: Types.Alerter;\n  UpdateAlerter: Types.Alerter;\n  RenameAlerter: Types.Update;\n\n  // ==== PROCEDURE ====\n  CreateProcedure: Types.Procedure;\n  CopyProcedure: Types.Procedure;\n  DeleteProcedure: Types.Procedure;\n  UpdateProcedure: Types.Procedure;\n  RenameProcedure: Types.Update;\n\n  // ==== ACTION ====\n  CreateAction: Types.Action;\n  CopyAction: Types.Action;\n  DeleteAction: Types.Action;\n  UpdateAction: Types.Action;\n  RenameAction: Types.Update;\n\n  // ==== SYNC ====\n  CreateResourceSync: Types.ResourceSync;\n  CopyResourceSync: Types.ResourceSync;\n  DeleteResourceSync: Types.ResourceSync;\n  UpdateResourceSync: Types.ResourceSync;\n  RenameResourceSync: Types.Update;\n  CommitSync: Types.Update;\n  WriteSyncFileContents: Types.Update;\n  RefreshResourceSyncPending: Types.ResourceSync;\n  CreateSyncWebhook: Types.CreateSyncWebhookResponse;\n  DeleteSyncWebhook: Types.DeleteSyncWebhookResponse;\n\n  // ==== TAG ====\n  CreateTag: Types.Tag;\n  DeleteTag: Types.Tag;\n  RenameTag: Types.Tag;\n  UpdateTagColor: Types.Tag;\n\n  // ==== VARIABLE ====\n  CreateVariable: Types.CreateVariableResponse;\n  UpdateVariableValue: Types.UpdateVariableValueResponse;\n  UpdateVariableDescription: Types.UpdateVariableDescriptionResponse;\n  UpdateVariableIsSecret: Types.UpdateVariableIsSecretResponse;\n  DeleteVariable: Types.DeleteVariableResponse;\n\n  // ==== PROVIDERS ====\n  CreateGitProviderAccount: Types.CreateGitProviderAccountResponse;\n  UpdateGitProviderAccount: Types.UpdateGitProviderAccountResponse;\n  DeleteGitProviderAccount: Types.DeleteGitProviderAccountResponse;\n  CreateDockerRegistryAccount: Types.CreateDockerRegistryAccountResponse;\n  UpdateDockerRegistryAccount: Types.UpdateDockerRegistryAccountResponse;\n  DeleteDockerRegistryAccount: Types.DeleteDockerRegistryAccountResponse;\n};\n\nexport type ExecuteResponses = {\n  // ==== SERVER ====\n  StartContainer: Types.Update;\n  RestartContainer: Types.Update;\n  PauseContainer: Types.Update;\n  UnpauseContainer: Types.Update;\n  StopContainer: Types.Update;\n  DestroyContainer: Types.Update;\n  StartAllContainers: Types.Update;\n  RestartAllContainers: Types.Update;\n  PauseAllContainers: Types.Update;\n  UnpauseAllContainers: Types.Update;\n  StopAllContainers: Types.Update;\n  PruneContainers: Types.Update;\n  DeleteNetwork: Types.Update;\n  PruneNetworks: Types.Update;\n  DeleteImage: Types.Update;\n  PruneImages: Types.Update;\n  DeleteVolume: Types.Update;\n  PruneVolumes: Types.Update;\n  PruneDockerBuilders: Types.Update;\n  PruneBuildx: Types.Update;\n  PruneSystem: Types.Update;\n\n  // ==== STACK ====\n  DeployStack: Types.Update;\n  BatchDeployStack: Types.BatchExecutionResponse;\n  DeployStackIfChanged: Types.Update;\n  BatchDeployStackIfChanged: Types.BatchExecutionResponse;\n  PullStack: Types.Update;\n  BatchPullStack: Types.BatchExecutionResponse;\n  StartStack: Types.Update;\n  RestartStack: Types.Update;\n  StopStack: Types.Update;\n  PauseStack: Types.Update;\n  UnpauseStack: Types.Update;\n  DestroyStack: Types.Update;\n  BatchDestroyStack: Types.BatchExecutionResponse;\n\n  // ==== DEPLOYMENT ====\n  Deploy: Types.Update;\n  BatchDeploy: Types.BatchExecutionResponse;\n  PullDeployment: Types.Update;\n  StartDeployment: Types.Update;\n  RestartDeployment: Types.Update;\n  PauseDeployment: Types.Update;\n  UnpauseDeployment: Types.Update;\n  StopDeployment: Types.Update;\n  DestroyDeployment: Types.Update;\n  BatchDestroyDeployment: Types.BatchExecutionResponse;\n\n  // ==== BUILD ====\n  RunBuild: Types.Update;\n  BatchRunBuild: Types.BatchExecutionResponse;\n  CancelBuild: Types.Update;\n\n  // ==== REPO ====\n  CloneRepo: Types.Update;\n  BatchCloneRepo: Types.BatchExecutionResponse;\n  PullRepo: Types.Update;\n  BatchPullRepo: Types.BatchExecutionResponse;\n  BuildRepo: Types.Update;\n  BatchBuildRepo: Types.BatchExecutionResponse;\n  CancelRepoBuild: Types.Update;\n\n  // ==== PROCEDURE ====\n  RunProcedure: Types.Update;\n  BatchRunProcedure: Types.BatchExecutionResponse;\n\n  // ==== ACTION ====\n  RunAction: Types.Update;\n  BatchRunAction: Types.BatchExecutionResponse;\n\n  // ==== SYNC ====\n  RunSync: Types.Update;\n\n  // ==== STACK Service ====\n  DeployStackService: Types.Update;\n  StartStackService: Types.Update;\n  RestartStackService: Types.Update;\n  StopStackService: Types.Update;\n  PauseStackService: Types.Update;\n  UnpauseStackService: Types.Update;\n  DestroyStackService: Types.Update;\n  RunStackService: Types.Update;\n  \n  // ==== ALERTER ====\n  TestAlerter: Types.Update;\n  SendAlert: Types.Update;\n\n  // ==== MAINTENANCE ====\n  ClearRepoCache: Types.Update;\n  BackupCoreDatabase: Types.Update;\n  GlobalAutoUpdate: Types.Update;\n};\n"
  },
  {
    "path": "client/core/ts/src/terminal.ts",
    "content": "import { ClientState, InitOptions } from \"./lib\";\nimport {\n  ConnectContainerExecQuery,\n  ConnectDeploymentExecQuery,\n  ConnectStackExecQuery,\n  ConnectTerminalQuery,\n  ExecuteContainerExecBody,\n  ExecuteDeploymentExecBody,\n  ExecuteStackExecBody,\n  ExecuteTerminalBody,\n  WsLoginMessage,\n} from \"./types\";\n\nexport type TerminalCallbacks = {\n  on_message?: (e: MessageEvent<any>) => void;\n  on_login?: () => void;\n  on_open?: () => void;\n  on_close?: () => void;\n};\n\nexport type ConnectExecQuery =\n  | {\n      type: \"container\";\n      query: ConnectContainerExecQuery;\n    }\n  | {\n      type: \"deployment\";\n      query: ConnectDeploymentExecQuery;\n    }\n  | {\n      type: \"stack\";\n      query: ConnectStackExecQuery;\n    };\n\nexport type ExecuteExecBody =\n  | {\n      type: \"container\";\n      body: ExecuteContainerExecBody;\n    }\n  | {\n      type: \"deployment\";\n      body: ExecuteDeploymentExecBody;\n    }\n  | {\n      type: \"stack\";\n      body: ExecuteStackExecBody;\n    };\n\nexport type ExecuteCallbacks = {\n  onLine?: (line: string) => void | Promise<void>;\n  onFinish?: (code: string) => void | Promise<void>;\n};\n\nexport const terminal_methods = (url: string, state: ClientState) => {\n  const connect_terminal = ({\n    query,\n    on_message,\n    on_login,\n    on_open,\n    on_close,\n  }: {\n    query: ConnectTerminalQuery;\n  } & TerminalCallbacks) => {\n    const url_query = new URLSearchParams(\n      query as any as Record<string, string>\n    ).toString();\n    const ws = new WebSocket(\n      url.replace(\"http\", \"ws\") + \"/ws/terminal?\" + url_query\n    );\n    // Handle login on websocket open\n    ws.onopen = () => {\n      const login_msg: WsLoginMessage = state.jwt\n        ? {\n            type: \"Jwt\",\n            params: {\n              jwt: state.jwt,\n            },\n          }\n        : {\n            type: \"ApiKeys\",\n            params: {\n              key: state.key!,\n              secret: state.secret!,\n            },\n          };\n      ws.send(JSON.stringify(login_msg));\n      on_open?.();\n    };\n\n    ws.onmessage = (e) => {\n      if (e.data == \"LOGGED_IN\") {\n        ws.binaryType = \"arraybuffer\";\n        ws.onmessage = (e) => on_message?.(e);\n        on_login?.();\n        return;\n      } else {\n        on_message?.(e);\n      }\n    };\n\n    ws.onclose = () => on_close?.();\n\n    return ws;\n  };\n\n  const execute_terminal = async (\n    request: ExecuteTerminalBody,\n    callbacks?: ExecuteCallbacks\n  ) => {\n    const stream = await execute_terminal_stream(request);\n    for await (const line of stream) {\n      if (line.startsWith(\"__KOMODO_EXIT_CODE\")) {\n        await callbacks?.onFinish?.(line.split(\":\")[1]);\n        return;\n      } else {\n        await callbacks?.onLine?.(line);\n      }\n    }\n    // This is hit if no __KOMODO_EXIT_CODE is sent, ie early exit\n    await callbacks?.onFinish?.(\"Early exit without code\");\n  };\n\n  const execute_terminal_stream = (request: ExecuteTerminalBody) =>\n    execute_stream(\"/terminal/execute\", request);\n\n  const connect_container_exec = ({\n    query,\n    ...callbacks\n  }: {\n    query: ConnectContainerExecQuery;\n  } & TerminalCallbacks) =>\n    connect_exec({ query: { type: \"container\", query }, ...callbacks });\n\n  const connect_deployment_exec = ({\n    query,\n    ...callbacks\n  }: {\n    query: ConnectDeploymentExecQuery;\n  } & TerminalCallbacks) =>\n    connect_exec({ query: { type: \"deployment\", query }, ...callbacks });\n\n  const connect_stack_exec = ({\n    query,\n    ...callbacks\n  }: {\n    query: ConnectStackExecQuery;\n  } & TerminalCallbacks) =>\n    connect_exec({ query: { type: \"stack\", query }, ...callbacks });\n\n  const connect_exec = ({\n    query: { type, query },\n    on_message,\n    on_login,\n    on_open,\n    on_close,\n  }: {\n    query: ConnectExecQuery;\n  } & TerminalCallbacks) => {\n    const url_query = new URLSearchParams(\n      query as any as Record<string, string>\n    ).toString();\n    const ws = new WebSocket(\n      url.replace(\"http\", \"ws\") + `/ws/${type}/terminal?` + url_query\n    );\n    // Handle login on websocket open\n    ws.onopen = () => {\n      const login_msg: WsLoginMessage = state.jwt\n        ? {\n            type: \"Jwt\",\n            params: {\n              jwt: state.jwt,\n            },\n          }\n        : {\n            type: \"ApiKeys\",\n            params: {\n              key: state.key!,\n              secret: state.secret!,\n            },\n          };\n      ws.send(JSON.stringify(login_msg));\n      on_open?.();\n    };\n\n    ws.onmessage = (e) => {\n      if (e.data == \"LOGGED_IN\") {\n        ws.binaryType = \"arraybuffer\";\n        ws.onmessage = (e) => on_message?.(e);\n        on_login?.();\n        return;\n      } else {\n        on_message?.(e);\n      }\n    };\n\n    ws.onclose = () => on_close?.();\n\n    return ws;\n  };\n\n  const execute_container_exec = (\n    body: ExecuteContainerExecBody,\n    callbacks?: ExecuteCallbacks\n  ) => execute_exec({ type: \"container\", body }, callbacks);\n\n  const execute_deployment_exec = (\n    body: ExecuteDeploymentExecBody,\n    callbacks?: ExecuteCallbacks\n  ) => execute_exec({ type: \"deployment\", body }, callbacks);\n\n  const execute_stack_exec = (\n    body: ExecuteStackExecBody,\n    callbacks?: ExecuteCallbacks\n  ) => execute_exec({ type: \"stack\", body }, callbacks);\n\n  const execute_exec = async (\n    request: ExecuteExecBody,\n    callbacks?: ExecuteCallbacks\n  ) => {\n    const stream = await execute_exec_stream(request);\n    for await (const line of stream) {\n      if (line.startsWith(\"__KOMODO_EXIT_CODE\")) {\n        await callbacks?.onFinish?.(line.split(\":\")[1]);\n        return;\n      } else {\n        await callbacks?.onLine?.(line);\n      }\n    }\n    // This is hit if no __KOMODO_EXIT_CODE is sent, ie early exit\n    await callbacks?.onFinish?.(\"Early exit without code\");\n  };\n\n  const execute_container_exec_stream = (body: ExecuteContainerExecBody) =>\n    execute_exec_stream({ type: \"container\", body });\n\n  const execute_deployment_exec_stream = (body: ExecuteDeploymentExecBody) =>\n    execute_exec_stream({ type: \"deployment\", body });\n\n  const execute_stack_exec_stream = (body: ExecuteStackExecBody) =>\n    execute_exec_stream({ type: \"stack\", body });\n\n  const execute_exec_stream = (request: ExecuteExecBody) =>\n    execute_stream(`/terminal/execute/${request.type}`, request.body);\n\n  const execute_stream = (path: string, request: any) =>\n    new Promise<AsyncIterable<string>>(async (res, rej) => {\n      try {\n        let response = await fetch(url + path, {\n          method: \"POST\",\n          body: JSON.stringify(request),\n          headers: {\n            ...(state.jwt\n              ? {\n                  authorization: state.jwt,\n                }\n              : state.key && state.secret\n              ? {\n                  \"x-api-key\": state.key,\n                  \"x-api-secret\": state.secret,\n                }\n              : {}),\n            \"content-type\": \"application/json\",\n          },\n        });\n        if (response.status === 200) {\n          if (response.body) {\n            const stream = response.body\n              .pipeThrough(new TextDecoderStream(\"utf-8\"))\n              .pipeThrough(\n                new TransformStream<string, string>({\n                  start(_controller) {\n                    this.tail = \"\";\n                  },\n                  transform(chunk, controller) {\n                    const data = this.tail + chunk; // prepend any carry‑over\n                    const parts = data.split(/\\r?\\n/); // split on CRLF or LF\n                    this.tail = parts.pop()!; // last item may be incomplete\n                    for (const line of parts) controller.enqueue(line);\n                  },\n                  flush(controller) {\n                    if (this.tail) controller.enqueue(this.tail); // final unterminated line\n                  },\n                } as Transformer<string, string> & { tail: string })\n              );\n            res(stream);\n          } else {\n            rej({\n              status: response.status,\n              result: { error: \"No response body\", trace: [] },\n            });\n          }\n        } else {\n          try {\n            const result = await response.json();\n            rej({ status: response.status, result });\n          } catch (error) {\n            rej({\n              status: response.status,\n              result: {\n                error: \"Failed to get response body\",\n                trace: [JSON.stringify(error)],\n              },\n              error,\n            });\n          }\n        }\n      } catch (error) {\n        rej({\n          status: 1,\n          result: {\n            error: \"Request failed with error\",\n            trace: [JSON.stringify(error)],\n          },\n          error,\n        });\n      }\n    });\n\n  return {\n    connect_terminal,\n    execute_terminal,\n    execute_terminal_stream,\n    connect_exec,\n    connect_container_exec,\n    execute_container_exec,\n    execute_container_exec_stream,\n    connect_deployment_exec,\n    execute_deployment_exec,\n    execute_deployment_exec_stream,\n    connect_stack_exec,\n    execute_stack_exec,\n    execute_stack_exec_stream,\n  };\n};\n"
  },
  {
    "path": "client/core/ts/src/types.ts",
    "content": "/*\n Generated by typeshare 1.13.3\n*/\n\nexport interface MongoIdObj {\n\t$oid: string;\n}\n\nexport type MongoId = MongoIdObj;\n\n/** The levels of permission that a User or UserGroup can have on a resource. */\nexport enum PermissionLevel {\n\t/** No permissions. */\n\tNone = \"None\",\n\t/** Can read resource information and config */\n\tRead = \"Read\",\n\t/** Can execute actions on the resource */\n\tExecute = \"Execute\",\n\t/** Can update the resource configuration */\n\tWrite = \"Write\",\n}\n\nexport interface PermissionLevelAndSpecifics {\n\tlevel: PermissionLevel;\n\tspecific: Array<SpecificPermission>;\n}\n\nexport type I64 = number;\n\nexport interface Resource<Config, Info> {\n\t/**\n\t * The Mongo ID of the resource.\n\t * This field is de/serialized from/to JSON as\n\t * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Resource<T>) }`\n\t */\n\t_id?: MongoId;\n\t/**\n\t * The resource name.\n\t * This is guaranteed unique among others of the same resource type.\n\t */\n\tname: string;\n\t/** A description for the resource */\n\tdescription?: string;\n\t/** Mark resource as a template */\n\ttemplate?: boolean;\n\t/** Tag Ids */\n\ttags?: string[];\n\t/** Resource-specific information (not user configurable). */\n\tinfo?: Info;\n\t/** Resource-specific configuration. */\n\tconfig?: Config;\n\t/**\n\t * Set a base permission level that all users will have on the\n\t * resource.\n\t */\n\tbase_permission?: PermissionLevelAndSpecifics | PermissionLevel;\n\t/** When description last updated */\n\tupdated_at?: I64;\n}\n\nexport enum ScheduleFormat {\n\tEnglish = \"English\",\n\tCron = \"Cron\",\n}\n\nexport enum FileFormat {\n\tKeyValue = \"key_value\",\n\tToml = \"toml\",\n\tYaml = \"yaml\",\n\tJson = \"json\",\n}\n\nexport interface ActionConfig {\n\t/** Whether this action should run at startup. */\n\trun_at_startup: boolean;\n\t/** Choose whether to specify schedule as regular CRON, or using the english to CRON parser. */\n\tschedule_format?: ScheduleFormat;\n\t/**\n\t * Optionally provide a schedule for the procedure to run on.\n\t * \n\t * There are 2 ways to specify a schedule:\n\t * \n\t * 1. Regular CRON expression:\n\t * \n\t * (second, minute, hour, day, month, day-of-week)\n\t * ```text\n\t * 0 0 0 1,15 * ?\n\t * ```\n\t * \n\t * 2. \"English\" expression via [english-to-cron](https://crates.io/crates/english-to-cron):\n\t * \n\t * ```text\n\t * at midnight on the 1st and 15th of the month\n\t * ```\n\t */\n\tschedule?: string;\n\t/**\n\t * Whether schedule is enabled if one is provided.\n\t * Can be used to temporarily disable the schedule.\n\t */\n\tschedule_enabled: boolean;\n\t/**\n\t * Optional. A TZ Identifier. If not provided, will use Core local timezone.\n\t * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n\t */\n\tschedule_timezone?: string;\n\t/** Whether to send alerts when the schedule was run. */\n\tschedule_alert: boolean;\n\t/** Whether to send alerts when this action fails. */\n\tfailure_alert: boolean;\n\t/** Whether incoming webhooks actually trigger action. */\n\twebhook_enabled: boolean;\n\t/**\n\t * Optionally provide an alternate webhook secret for this procedure.\n\t * If its an empty string, use the default secret from the config.\n\t */\n\twebhook_secret?: string;\n\t/**\n\t * Whether deno will be instructed to reload all dependencies,\n\t * this can usually be kept false outside of development.\n\t */\n\treload_deno_deps?: boolean;\n\t/**\n\t * Typescript file contents using pre-initialized `komodo` client.\n\t * Supports variable / secret interpolation.\n\t */\n\tfile_contents?: string;\n\t/**\n\t * Specify the format in which the arguments are defined.\n\t * Default: `key_value` (like environment)\n\t */\n\targuments_format?: FileFormat;\n\t/** Default arguments to give to the Action for use in the script at `ARGS`. */\n\targuments?: string;\n}\n\n/** Represents an empty json object: `{}` */\nexport interface NoData {\n}\n\nexport type Action = Resource<ActionConfig, NoData>;\n\nexport interface ResourceListItem<Info> {\n\t/** The resource id */\n\tid: string;\n\t/** The resource type, ie `Server` or `Deployment` */\n\ttype: ResourceTarget[\"type\"];\n\t/** The resource name */\n\tname: string;\n\t/** Whether resource is a template */\n\ttemplate: boolean;\n\t/** Tag Ids */\n\ttags: string[];\n\t/** Resource specific info */\n\tinfo: Info;\n}\n\nexport enum ActionState {\n\t/** Unknown case */\n\tUnknown = \"Unknown\",\n\t/** Last clone / pull successful (or never cloned) */\n\tOk = \"Ok\",\n\t/** Last clone / pull failed */\n\tFailed = \"Failed\",\n\t/** Currently running */\n\tRunning = \"Running\",\n}\n\nexport interface ActionListItemInfo {\n\t/** Whether last action run successful */\n\tstate: ActionState;\n\t/** Action last successful run timestamp in ms. */\n\tlast_run_at?: I64;\n\t/**\n\t * If the action has schedule enabled, this is the\n\t * next scheduled run time in unix ms.\n\t */\n\tnext_scheduled_run?: I64;\n\t/**\n\t * If there is an error parsing schedule expression,\n\t * it will be given here.\n\t */\n\tschedule_error?: string;\n}\n\nexport type ActionListItem = ResourceListItem<ActionListItemInfo>;\n\nexport enum TemplatesQueryBehavior {\n\t/** Include templates in results. Default. */\n\tInclude = \"Include\",\n\t/** Exclude templates from results. */\n\tExclude = \"Exclude\",\n\t/** Results *only* includes templates. */\n\tOnly = \"Only\",\n}\n\nexport enum TagQueryBehavior {\n\t/** Returns resources which have strictly all the tags */\n\tAll = \"All\",\n\t/** Returns resources which have one or more of the tags */\n\tAny = \"Any\",\n}\n\n/** Passing empty Vec is the same as not filtering by that field */\nexport interface ResourceQuery<T> {\n\tnames?: string[];\n\ttemplates?: TemplatesQueryBehavior;\n\t/** Pass Vec of tag ids or tag names */\n\ttags?: string[];\n\t/** 'All' or 'Any' */\n\ttag_behavior?: TagQueryBehavior;\n\tspecific?: T;\n}\n\nexport interface ActionQuerySpecifics {\n}\n\nexport type ActionQuery = ResourceQuery<ActionQuerySpecifics>;\n\nexport type AlerterEndpoint = \n\t/** Send alert serialized to JSON to an http endpoint. */\n\t| { type: \"Custom\", params: CustomAlerterEndpoint }\n\t/** Send alert to a Slack app */\n\t| { type: \"Slack\", params: SlackAlerterEndpoint }\n\t/** Send alert to a Discord app */\n\t| { type: \"Discord\", params: DiscordAlerterEndpoint }\n\t/** Send alert to Ntfy */\n\t| { type: \"Ntfy\", params: NtfyAlerterEndpoint }\n\t/** Send alert to Pushover */\n\t| { type: \"Pushover\", params: PushoverAlerterEndpoint };\n\n/** Used to reference a specific resource across all resource types */\nexport type ResourceTarget = \n\t| { type: \"System\", id: string }\n\t| { type: \"Server\", id: string }\n\t| { type: \"Stack\", id: string }\n\t| { type: \"Deployment\", id: string }\n\t| { type: \"Build\", id: string }\n\t| { type: \"Repo\", id: string }\n\t| { type: \"Procedure\", id: string }\n\t| { type: \"Action\", id: string }\n\t| { type: \"Builder\", id: string }\n\t| { type: \"Alerter\", id: string }\n\t| { type: \"ResourceSync\", id: string };\n\n/** Types of maintenance schedules */\nexport enum MaintenanceScheduleType {\n\t/** Daily at the specified time */\n\tDaily = \"Daily\",\n\t/** Weekly on the specified day and time */\n\tWeekly = \"Weekly\",\n\t/** One-time maintenance on a specific date and time */\n\tOneTime = \"OneTime\",\n}\n\n/** Represents a scheduled maintenance window */\nexport interface MaintenanceWindow {\n\t/** Name for the maintenance window (required) */\n\tname: string;\n\t/** Description of what maintenance is performed (optional) */\n\tdescription?: string;\n\t/**\n\t * The type of maintenance schedule:\n\t * - Daily (default)\n\t * - Weekly\n\t * - OneTime\n\t */\n\tschedule_type?: MaintenanceScheduleType;\n\t/** For Weekly schedules: Specify the day of the week (Monday, Tuesday, etc.) */\n\tday_of_week?: string;\n\t/** For OneTime window: ISO 8601 date format (YYYY-MM-DD) */\n\tdate?: string;\n\t/** Start hour in 24-hour format (0-23) (optional, defaults to 0) */\n\thour?: number;\n\t/** Start minute (0-59) (optional, defaults to 0) */\n\tminute?: number;\n\t/** Duration of the maintenance window in minutes (required) */\n\tduration_minutes: number;\n\t/**\n\t * Timezone for maintenance window specificiation.\n\t * If empty, will use Core timezone.\n\t */\n\ttimezone?: string;\n\t/** Whether this maintenance window is currently enabled */\n\tenabled: boolean;\n}\n\nexport interface AlerterConfig {\n\t/** Whether the alerter is enabled */\n\tenabled?: boolean;\n\t/**\n\t * Where to route the alert messages.\n\t * \n\t * Default: Custom endpoint `http://localhost:7000`\n\t */\n\tendpoint?: AlerterEndpoint;\n\t/**\n\t * Only send specific alert types.\n\t * If empty, will send all alert types.\n\t */\n\talert_types?: AlertData[\"type\"][];\n\t/**\n\t * Only send alerts on specific resources.\n\t * If empty, will send alerts for all resources.\n\t */\n\tresources?: ResourceTarget[];\n\t/** DON'T send alerts on these resources. */\n\texcept_resources?: ResourceTarget[];\n\t/** Scheduled maintenance windows during which alerts will be suppressed. */\n\tmaintenance_windows?: MaintenanceWindow[];\n}\n\nexport type Alerter = Resource<AlerterConfig, undefined>;\n\nexport interface AlerterListItemInfo {\n\t/** Whether alerter is enabled for sending alerts */\n\tenabled: boolean;\n\t/** The type of the alerter, eg. `Slack`, `Custom` */\n\tendpoint_type: AlerterEndpoint[\"type\"];\n}\n\nexport type AlerterListItem = ResourceListItem<AlerterListItemInfo>;\n\nexport interface AlerterQuerySpecifics {\n\t/**\n\t * Filter alerters by enabled.\n\t * - `None`: Don't filter by enabled\n\t * - `Some(true)`: Only include alerts with `enabled: true`\n\t * - `Some(false)`: Only include alerts with `enabled: false`\n\t */\n\tenabled?: boolean;\n\t/**\n\t * Only include alerters with these endpoint types.\n\t * If empty, don't filter by enpoint type.\n\t */\n\ttypes: AlerterEndpoint[\"type\"][];\n}\n\nexport type AlerterQuery = ResourceQuery<AlerterQuerySpecifics>;\n\nexport type BatchExecutionResponseItem = \n\t| { status: \"Ok\", data: Update }\n\t| { status: \"Err\", data: BatchExecutionResponseItemErr };\n\nexport type BatchExecutionResponse = BatchExecutionResponseItem[];\n\nexport enum Operation {\n\tNone = \"None\",\n\tCreateServer = \"CreateServer\",\n\tUpdateServer = \"UpdateServer\",\n\tDeleteServer = \"DeleteServer\",\n\tRenameServer = \"RenameServer\",\n\tStartContainer = \"StartContainer\",\n\tRestartContainer = \"RestartContainer\",\n\tPauseContainer = \"PauseContainer\",\n\tUnpauseContainer = \"UnpauseContainer\",\n\tStopContainer = \"StopContainer\",\n\tDestroyContainer = \"DestroyContainer\",\n\tStartAllContainers = \"StartAllContainers\",\n\tRestartAllContainers = \"RestartAllContainers\",\n\tPauseAllContainers = \"PauseAllContainers\",\n\tUnpauseAllContainers = \"UnpauseAllContainers\",\n\tStopAllContainers = \"StopAllContainers\",\n\tPruneContainers = \"PruneContainers\",\n\tCreateNetwork = \"CreateNetwork\",\n\tDeleteNetwork = \"DeleteNetwork\",\n\tPruneNetworks = \"PruneNetworks\",\n\tDeleteImage = \"DeleteImage\",\n\tPruneImages = \"PruneImages\",\n\tDeleteVolume = \"DeleteVolume\",\n\tPruneVolumes = \"PruneVolumes\",\n\tPruneDockerBuilders = \"PruneDockerBuilders\",\n\tPruneBuildx = \"PruneBuildx\",\n\tPruneSystem = \"PruneSystem\",\n\tCreateStack = \"CreateStack\",\n\tUpdateStack = \"UpdateStack\",\n\tRenameStack = \"RenameStack\",\n\tDeleteStack = \"DeleteStack\",\n\tWriteStackContents = \"WriteStackContents\",\n\tRefreshStackCache = \"RefreshStackCache\",\n\tPullStack = \"PullStack\",\n\tDeployStack = \"DeployStack\",\n\tStartStack = \"StartStack\",\n\tRestartStack = \"RestartStack\",\n\tPauseStack = \"PauseStack\",\n\tUnpauseStack = \"UnpauseStack\",\n\tStopStack = \"StopStack\",\n\tDestroyStack = \"DestroyStack\",\n\tRunStackService = \"RunStackService\",\n\tDeployStackService = \"DeployStackService\",\n\tPullStackService = \"PullStackService\",\n\tStartStackService = \"StartStackService\",\n\tRestartStackService = \"RestartStackService\",\n\tPauseStackService = \"PauseStackService\",\n\tUnpauseStackService = \"UnpauseStackService\",\n\tStopStackService = \"StopStackService\",\n\tDestroyStackService = \"DestroyStackService\",\n\tCreateDeployment = \"CreateDeployment\",\n\tUpdateDeployment = \"UpdateDeployment\",\n\tRenameDeployment = \"RenameDeployment\",\n\tDeleteDeployment = \"DeleteDeployment\",\n\tDeploy = \"Deploy\",\n\tPullDeployment = \"PullDeployment\",\n\tStartDeployment = \"StartDeployment\",\n\tRestartDeployment = \"RestartDeployment\",\n\tPauseDeployment = \"PauseDeployment\",\n\tUnpauseDeployment = \"UnpauseDeployment\",\n\tStopDeployment = \"StopDeployment\",\n\tDestroyDeployment = \"DestroyDeployment\",\n\tCreateBuild = \"CreateBuild\",\n\tUpdateBuild = \"UpdateBuild\",\n\tRenameBuild = \"RenameBuild\",\n\tDeleteBuild = \"DeleteBuild\",\n\tRunBuild = \"RunBuild\",\n\tCancelBuild = \"CancelBuild\",\n\tWriteDockerfile = \"WriteDockerfile\",\n\tCreateRepo = \"CreateRepo\",\n\tUpdateRepo = \"UpdateRepo\",\n\tRenameRepo = \"RenameRepo\",\n\tDeleteRepo = \"DeleteRepo\",\n\tCloneRepo = \"CloneRepo\",\n\tPullRepo = \"PullRepo\",\n\tBuildRepo = \"BuildRepo\",\n\tCancelRepoBuild = \"CancelRepoBuild\",\n\tCreateProcedure = \"CreateProcedure\",\n\tUpdateProcedure = \"UpdateProcedure\",\n\tRenameProcedure = \"RenameProcedure\",\n\tDeleteProcedure = \"DeleteProcedure\",\n\tRunProcedure = \"RunProcedure\",\n\tCreateAction = \"CreateAction\",\n\tUpdateAction = \"UpdateAction\",\n\tRenameAction = \"RenameAction\",\n\tDeleteAction = \"DeleteAction\",\n\tRunAction = \"RunAction\",\n\tCreateBuilder = \"CreateBuilder\",\n\tUpdateBuilder = \"UpdateBuilder\",\n\tRenameBuilder = \"RenameBuilder\",\n\tDeleteBuilder = \"DeleteBuilder\",\n\tCreateAlerter = \"CreateAlerter\",\n\tUpdateAlerter = \"UpdateAlerter\",\n\tRenameAlerter = \"RenameAlerter\",\n\tDeleteAlerter = \"DeleteAlerter\",\n\tTestAlerter = \"TestAlerter\",\n\tSendAlert = \"SendAlert\",\n\tCreateResourceSync = \"CreateResourceSync\",\n\tUpdateResourceSync = \"UpdateResourceSync\",\n\tRenameResourceSync = \"RenameResourceSync\",\n\tDeleteResourceSync = \"DeleteResourceSync\",\n\tWriteSyncContents = \"WriteSyncContents\",\n\tCommitSync = \"CommitSync\",\n\tRunSync = \"RunSync\",\n\tClearRepoCache = \"ClearRepoCache\",\n\tBackupCoreDatabase = \"BackupCoreDatabase\",\n\tGlobalAutoUpdate = \"GlobalAutoUpdate\",\n\tCreateVariable = \"CreateVariable\",\n\tUpdateVariableValue = \"UpdateVariableValue\",\n\tDeleteVariable = \"DeleteVariable\",\n\tCreateGitProviderAccount = \"CreateGitProviderAccount\",\n\tUpdateGitProviderAccount = \"UpdateGitProviderAccount\",\n\tDeleteGitProviderAccount = \"DeleteGitProviderAccount\",\n\tCreateDockerRegistryAccount = \"CreateDockerRegistryAccount\",\n\tUpdateDockerRegistryAccount = \"UpdateDockerRegistryAccount\",\n\tDeleteDockerRegistryAccount = \"DeleteDockerRegistryAccount\",\n}\n\n/** Represents the output of some command being run */\nexport interface Log {\n\t/** A label for the log */\n\tstage: string;\n\t/** The command which was executed */\n\tcommand: string;\n\t/** The output of the command in the standard channel */\n\tstdout: string;\n\t/** The output of the command in the error channel */\n\tstderr: string;\n\t/** Whether the command run was successful */\n\tsuccess: boolean;\n\t/** The start time of the command execution */\n\tstart_ts: I64;\n\t/** The end time of the command execution */\n\tend_ts: I64;\n}\n\n/** An update's status */\nexport enum UpdateStatus {\n\t/** The run is in the system but hasn't started yet */\n\tQueued = \"Queued\",\n\t/** The run is currently running */\n\tInProgress = \"InProgress\",\n\t/** The run is complete */\n\tComplete = \"Complete\",\n}\n\nexport interface Version {\n\tmajor: number;\n\tminor: number;\n\tpatch: number;\n}\n\n/** Represents an action performed by Komodo. */\nexport interface Update {\n\t/**\n\t * The Mongo ID of the update.\n\t * This field is de/serialized from/to JSON as\n\t * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Update) }`\n\t */\n\t_id?: MongoId;\n\t/** The operation performed */\n\toperation: Operation;\n\t/** The time the operation started */\n\tstart_ts: I64;\n\t/** Whether the operation was successful */\n\tsuccess: boolean;\n\t/**\n\t * The user id that triggered the update.\n\t * \n\t * Also can take these values for operations triggered automatically:\n\t * - `Procedure`: The operation was triggered as part of a procedure run\n\t * - `Github`: The operation was triggered by a github webhook\n\t * - `Auto Redeploy`: The operation (always `Deploy`) was triggered by an attached build finishing.\n\t */\n\toperator: string;\n\t/** The target resource to which this update refers */\n\ttarget: ResourceTarget;\n\t/** Logs produced as the operation is performed */\n\tlogs: Log[];\n\t/** The time the operation completed. */\n\tend_ts?: I64;\n\t/**\n\t * The status of the update\n\t * - `Queued`\n\t * - `InProgress`\n\t * - `Complete`\n\t */\n\tstatus: UpdateStatus;\n\t/** An optional version on the update, ie build version or deployed version. */\n\tversion?: Version;\n\t/** An optional commit hash associated with the update, ie cloned hash or deployed hash. */\n\tcommit_hash?: string;\n\t/** Some unstructured, operation specific data. Not for general usage. */\n\tother_data?: string;\n\t/** If the update is for resource config update, give the previous toml contents */\n\tprev_toml?: string;\n\t/** If the update is for resource config update, give the current (at time of Update) toml contents */\n\tcurrent_toml?: string;\n}\n\nexport type BoxUpdate = Update;\n\n/** Configuration for an image registry */\nexport interface ImageRegistryConfig {\n\t/**\n\t * Specify the registry provider domain, eg `docker.io`.\n\t * If not provided, will not push to any registry.\n\t */\n\tdomain?: string;\n\t/** Specify an account to use with the registry. */\n\taccount?: string;\n\t/**\n\t * Optional. Specify an organization to push the image under.\n\t * Empty string means no organization.\n\t */\n\torganization?: string;\n}\n\nexport interface SystemCommand {\n\tpath?: string;\n\tcommand?: string;\n}\n\n/** The build configuration. */\nexport interface BuildConfig {\n\t/** Which builder is used to build the image. */\n\tbuilder_id?: string;\n\t/** The current version of the build. */\n\tversion?: Version;\n\t/**\n\t * Whether to automatically increment the patch on every build.\n\t * Default is `true`\n\t */\n\tauto_increment_version: boolean;\n\t/**\n\t * An alternate name for the image pushed to the repository.\n\t * If this is empty, it will use the build name.\n\t * \n\t * Can be used in conjunction with `image_tag` to direct multiple builds\n\t * with different configs to push to the same image registry, under different,\n\t * independantly versioned tags.\n\t */\n\timage_name?: string;\n\t/**\n\t * An extra tag put after the build version, for the image pushed to the repository.\n\t * Eg. in image tag of `aarch64` would push to moghtech/komodo-core:1.13.2-aarch64.\n\t * If this is empty, the image tag will just be the build version.\n\t * \n\t * Can be used in conjunction with `image_name` to direct multiple builds\n\t * with different configs to push to the same image registry, under different,\n\t * independantly versioned tags.\n\t */\n\timage_tag?: string;\n\t/** Push `:latest` / `:latest-image_tag` tags. */\n\tinclude_latest_tag: boolean;\n\t/** Push build version semver `:1.19.5` + `1.19` / `:1.19.5-image_tag` tags. */\n\tinclude_version_tags: boolean;\n\t/** Push commit hash `:a6v8h83` / `:a6v8h83-image_tag` tags. */\n\tinclude_commit_tag: boolean;\n\t/** Configure quick links that are displayed in the resource header */\n\tlinks?: string[];\n\t/** Choose a Komodo Repo (Resource) to source the build files. */\n\tlinked_repo?: string;\n\t/** The git provider domain. Default: github.com */\n\tgit_provider: string;\n\t/**\n\t * Whether to use https to clone the repo (versus http). Default: true\n\t * \n\t * Note. Komodo does not currently support cloning repos via ssh.\n\t */\n\tgit_https: boolean;\n\t/**\n\t * The git account used to access private repos.\n\t * Passing empty string can only clone public repos.\n\t * \n\t * Note. A token for the account must be available in the core config or the builder server's periphery config\n\t * for the configured git provider.\n\t */\n\tgit_account?: string;\n\t/** The repo used as the source of the build. */\n\trepo?: string;\n\t/** The branch of the repo. */\n\tbranch: string;\n\t/** Optionally set a specific commit hash. */\n\tcommit?: string;\n\t/** Whether incoming webhooks actually trigger action. */\n\twebhook_enabled: boolean;\n\t/**\n\t * Optionally provide an alternate webhook secret for this build.\n\t * If its an empty string, use the default secret from the config.\n\t */\n\twebhook_secret?: string;\n\t/**\n\t * If this is checked, the build will source the files on the host.\n\t * Use `build_path` and `dockerfile_path` to specify the path on the host.\n\t * This is useful for those who wish to setup their files on the host,\n\t * rather than defining the contents in UI or in a git repo.\n\t */\n\tfiles_on_host?: boolean;\n\t/**\n\t * The path of the docker build context relative to the root of the repo.\n\t * Default: \".\" (the root of the repo).\n\t */\n\tbuild_path: string;\n\t/** The path of the dockerfile relative to the build path. */\n\tdockerfile_path: string;\n\t/**\n\t * Configuration for the registry/s to push the built image to.\n\t * The first registry in this list will be used with attached Deployments.\n\t */\n\timage_registry?: ImageRegistryConfig[];\n\t/** Whether to skip secret interpolation in the build_args. */\n\tskip_secret_interp?: boolean;\n\t/** Whether to use buildx to build (eg `docker buildx build ...`) */\n\tuse_buildx?: boolean;\n\t/** Any extra docker cli arguments to be included in the build command */\n\textra_args?: string[];\n\t/** The optional command run after repo clone and before docker build. */\n\tpre_build?: SystemCommand;\n\t/**\n\t * UI defined dockerfile contents.\n\t * Supports variable / secret interpolation.\n\t */\n\tdockerfile?: string;\n\t/**\n\t * Docker build arguments.\n\t * \n\t * These values are visible in the final image by running `docker inspect`.\n\t */\n\tbuild_args?: string;\n\t/**\n\t * Secret arguments.\n\t * \n\t * These values remain hidden in the final image by using\n\t * docker secret mounts. See <https://docs.docker.com/build/building/secrets>.\n\t * \n\t * The values can be used in RUN commands:\n\t * ```sh\n\t * RUN --mount=type=secret,id=SECRET_KEY \\\n\t * SECRET_KEY=$(cat /run/secrets/SECRET_KEY) ...\n\t * ```\n\t */\n\tsecret_args?: string;\n\t/** Docker labels */\n\tlabels?: string;\n}\n\nexport interface BuildInfo {\n\t/** The timestamp build was last built. */\n\tlast_built_at: I64;\n\t/** Latest built short commit hash, or null. */\n\tbuilt_hash?: string;\n\t/** Latest built commit message, or null. Only for repo based stacks */\n\tbuilt_message?: string;\n\t/**\n\t * The last built dockerfile contents.\n\t * This is updated whenever Komodo successfully runs the build.\n\t */\n\tbuilt_contents?: string;\n\t/** The absolute path to the file */\n\tremote_path?: string;\n\t/**\n\t * The remote dockerfile contents, whether on host or in repo.\n\t * This is updated whenever Komodo refreshes the build cache.\n\t * It will be empty if the dockerfile is defined directly in the build config.\n\t */\n\tremote_contents?: string;\n\t/** If there was an error in getting the remote contents, it will be here. */\n\tremote_error?: string;\n\t/** Latest remote short commit hash, or null. */\n\tlatest_hash?: string;\n\t/** Latest remote commit message, or null */\n\tlatest_message?: string;\n}\n\nexport type Build = Resource<BuildConfig, BuildInfo>;\n\nexport enum BuildState {\n\t/** Currently building */\n\tBuilding = \"Building\",\n\t/** Last build successful (or never built) */\n\tOk = \"Ok\",\n\t/** Last build failed */\n\tFailed = \"Failed\",\n\t/** Other case */\n\tUnknown = \"Unknown\",\n}\n\nexport interface BuildListItemInfo {\n\t/** State of the build. Reflects whether most recent build successful. */\n\tstate: BuildState;\n\t/** Unix timestamp in milliseconds of last build */\n\tlast_built_at: I64;\n\t/** The current version of the build */\n\tversion: Version;\n\t/** The builder attached to build. */\n\tbuilder_id: string;\n\t/** Whether build is in files on host mode. */\n\tfiles_on_host: boolean;\n\t/** Whether build has UI defined dockerfile contents */\n\tdockerfile_contents: boolean;\n\t/** Linked repo, if one is attached. */\n\tlinked_repo: string;\n\t/** The git provider domain */\n\tgit_provider: string;\n\t/** The repo used as the source of the build */\n\trepo: string;\n\t/** The branch of the repo */\n\tbranch: string;\n\t/** Full link to the repo. */\n\trepo_link: string;\n\t/** Latest built short commit hash, or null. */\n\tbuilt_hash?: string;\n\t/** Latest short commit hash, or null. Only for repo based stacks */\n\tlatest_hash?: string;\n\t/** The first listed image registry domain */\n\timage_registry_domain?: string;\n}\n\nexport type BuildListItem = ResourceListItem<BuildListItemInfo>;\n\nexport interface BuildQuerySpecifics {\n\tbuilder_ids?: string[];\n\trepos?: string[];\n\t/**\n\t * query for builds last built more recently than this timestamp\n\t * defaults to 0 which is a no op\n\t */\n\tbuilt_since?: I64;\n}\n\nexport type BuildQuery = ResourceQuery<BuildQuerySpecifics>;\n\nexport type BuilderConfig = \n\t/** Use a Periphery address as a Builder. */\n\t| { type: \"Url\", params: UrlBuilderConfig }\n\t/** Use a connected server as a Builder. */\n\t| { type: \"Server\", params: ServerBuilderConfig }\n\t/** Use EC2 instances spawned on demand as a Builder. */\n\t| { type: \"Aws\", params: AwsBuilderConfig };\n\nexport type Builder = Resource<BuilderConfig, undefined>;\n\nexport interface BuilderListItemInfo {\n\t/** 'Url', 'Server', or 'Aws' */\n\tbuilder_type: string;\n\t/**\n\t * If 'Url': null\n\t * If 'Server': the server id\n\t * If 'Aws': the instance type (eg. c5.xlarge)\n\t */\n\tinstance_type?: string;\n}\n\nexport type BuilderListItem = ResourceListItem<BuilderListItemInfo>;\n\nexport interface BuilderQuerySpecifics {\n}\n\nexport type BuilderQuery = ResourceQuery<BuilderQuerySpecifics>;\n\n/** A wrapper for all Komodo exections. */\nexport type Execution = \n\t/** The \"null\" execution. Does nothing. */\n\t| { type: \"None\", params: NoData }\n\t/** Run the target action. (alias: `action`, `ac`) */\n\t| { type: \"RunAction\", params: RunAction }\n\t| { type: \"BatchRunAction\", params: BatchRunAction }\n\t/** Run the target procedure. (alias: `procedure`, `pr`) */\n\t| { type: \"RunProcedure\", params: RunProcedure }\n\t| { type: \"BatchRunProcedure\", params: BatchRunProcedure }\n\t/** Run the target build. (alias: `build`, `bd`) */\n\t| { type: \"RunBuild\", params: RunBuild }\n\t| { type: \"BatchRunBuild\", params: BatchRunBuild }\n\t| { type: \"CancelBuild\", params: CancelBuild }\n\t/** Deploy the target deployment. (alias: `dp`) */\n\t| { type: \"Deploy\", params: Deploy }\n\t| { type: \"BatchDeploy\", params: BatchDeploy }\n\t| { type: \"PullDeployment\", params: PullDeployment }\n\t| { type: \"StartDeployment\", params: StartDeployment }\n\t| { type: \"RestartDeployment\", params: RestartDeployment }\n\t| { type: \"PauseDeployment\", params: PauseDeployment }\n\t| { type: \"UnpauseDeployment\", params: UnpauseDeployment }\n\t| { type: \"StopDeployment\", params: StopDeployment }\n\t| { type: \"DestroyDeployment\", params: DestroyDeployment }\n\t| { type: \"BatchDestroyDeployment\", params: BatchDestroyDeployment }\n\t/** Clone the target repo */\n\t| { type: \"CloneRepo\", params: CloneRepo }\n\t| { type: \"BatchCloneRepo\", params: BatchCloneRepo }\n\t| { type: \"PullRepo\", params: PullRepo }\n\t| { type: \"BatchPullRepo\", params: BatchPullRepo }\n\t| { type: \"BuildRepo\", params: BuildRepo }\n\t| { type: \"BatchBuildRepo\", params: BatchBuildRepo }\n\t| { type: \"CancelRepoBuild\", params: CancelRepoBuild }\n\t| { type: \"StartContainer\", params: StartContainer }\n\t| { type: \"RestartContainer\", params: RestartContainer }\n\t| { type: \"PauseContainer\", params: PauseContainer }\n\t| { type: \"UnpauseContainer\", params: UnpauseContainer }\n\t| { type: \"StopContainer\", params: StopContainer }\n\t| { type: \"DestroyContainer\", params: DestroyContainer }\n\t| { type: \"StartAllContainers\", params: StartAllContainers }\n\t| { type: \"RestartAllContainers\", params: RestartAllContainers }\n\t| { type: \"PauseAllContainers\", params: PauseAllContainers }\n\t| { type: \"UnpauseAllContainers\", params: UnpauseAllContainers }\n\t| { type: \"StopAllContainers\", params: StopAllContainers }\n\t| { type: \"PruneContainers\", params: PruneContainers }\n\t| { type: \"DeleteNetwork\", params: DeleteNetwork }\n\t| { type: \"PruneNetworks\", params: PruneNetworks }\n\t| { type: \"DeleteImage\", params: DeleteImage }\n\t| { type: \"PruneImages\", params: PruneImages }\n\t| { type: \"DeleteVolume\", params: DeleteVolume }\n\t| { type: \"PruneVolumes\", params: PruneVolumes }\n\t| { type: \"PruneDockerBuilders\", params: PruneDockerBuilders }\n\t| { type: \"PruneBuildx\", params: PruneBuildx }\n\t| { type: \"PruneSystem\", params: PruneSystem }\n\t/** Execute a Resource Sync. (alias: `sync`) */\n\t| { type: \"RunSync\", params: RunSync }\n\t/** Commit a Resource Sync. (alias: `commit`) */\n\t| { type: \"CommitSync\", params: CommitSync }\n\t/** Deploy the target stack. (alias: `stack`, `st`) */\n\t| { type: \"DeployStack\", params: DeployStack }\n\t| { type: \"BatchDeployStack\", params: BatchDeployStack }\n\t| { type: \"DeployStackIfChanged\", params: DeployStackIfChanged }\n\t| { type: \"BatchDeployStackIfChanged\", params: BatchDeployStackIfChanged }\n\t| { type: \"PullStack\", params: PullStack }\n\t| { type: \"BatchPullStack\", params: BatchPullStack }\n\t| { type: \"StartStack\", params: StartStack }\n\t| { type: \"RestartStack\", params: RestartStack }\n\t| { type: \"PauseStack\", params: PauseStack }\n\t| { type: \"UnpauseStack\", params: UnpauseStack }\n\t| { type: \"StopStack\", params: StopStack }\n\t| { type: \"DestroyStack\", params: DestroyStack }\n\t| { type: \"BatchDestroyStack\", params: BatchDestroyStack }\n\t| { type: \"RunStackService\", params: RunStackService }\n\t| { type: \"TestAlerter\", params: TestAlerter }\n\t| { type: \"SendAlert\", params: SendAlert }\n\t| { type: \"ClearRepoCache\", params: ClearRepoCache }\n\t| { type: \"BackupCoreDatabase\", params: BackupCoreDatabase }\n\t| { type: \"GlobalAutoUpdate\", params: GlobalAutoUpdate }\n\t| { type: \"Sleep\", params: Sleep };\n\n/** Allows to enable / disabled procedures in the sequence / parallel vec on the fly */\nexport interface EnabledExecution {\n\t/** The execution request to run. */\n\texecution: Execution;\n\t/** Whether the execution is enabled to run in the procedure. */\n\tenabled: boolean;\n}\n\n/** A single stage of a procedure. Runs a list of executions in parallel. */\nexport interface ProcedureStage {\n\t/** A name for the procedure */\n\tname: string;\n\t/** Whether the stage should be run as part of the procedure. */\n\tenabled: boolean;\n\t/** The executions in the stage */\n\texecutions?: EnabledExecution[];\n}\n\n/** Config for the [Procedure] */\nexport interface ProcedureConfig {\n\t/** The stages to be run by the procedure. */\n\tstages?: ProcedureStage[];\n\t/** Choose whether to specify schedule as regular CRON, or using the english to CRON parser. */\n\tschedule_format?: ScheduleFormat;\n\t/**\n\t * Optionally provide a schedule for the procedure to run on.\n\t * \n\t * There are 2 ways to specify a schedule:\n\t * \n\t * 1. Regular CRON expression:\n\t * \n\t * (second, minute, hour, day, month, day-of-week)\n\t * ```text\n\t * 0 0 0 1,15 * ?\n\t * ```\n\t * \n\t * 2. \"English\" expression via [english-to-cron](https://crates.io/crates/english-to-cron):\n\t * \n\t * ```text\n\t * at midnight on the 1st and 15th of the month\n\t * ```\n\t */\n\tschedule?: string;\n\t/**\n\t * Whether schedule is enabled if one is provided.\n\t * Can be used to temporarily disable the schedule.\n\t */\n\tschedule_enabled: boolean;\n\t/**\n\t * Optional. A TZ Identifier. If not provided, will use Core local timezone.\n\t * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n\t */\n\tschedule_timezone?: string;\n\t/** Whether to send alerts when the schedule was run. */\n\tschedule_alert: boolean;\n\t/** Whether to send alerts when this procedure fails. */\n\tfailure_alert: boolean;\n\t/** Whether incoming webhooks actually trigger action. */\n\twebhook_enabled: boolean;\n\t/**\n\t * Optionally provide an alternate webhook secret for this procedure.\n\t * If its an empty string, use the default secret from the config.\n\t */\n\twebhook_secret?: string;\n}\n\n/**\n * Procedures run a series of stages sequentially, where\n * each stage runs executions in parallel.\n */\nexport type Procedure = Resource<ProcedureConfig, undefined>;\n\nexport type CopyProcedureResponse = Procedure;\n\nexport type CreateActionWebhookResponse = NoData;\n\n/** Response for [CreateApiKey]. */\nexport interface CreateApiKeyResponse {\n\t/** X-API-KEY */\n\tkey: string;\n\t/**\n\t * X-API-SECRET\n\t * \n\t * Note.\n\t * There is no way to get the secret again after it is distributed in this message\n\t */\n\tsecret: string;\n}\n\nexport type CreateApiKeyForServiceUserResponse = CreateApiKeyResponse;\n\nexport type CreateBuildWebhookResponse = NoData;\n\n/** Configuration to access private image repositories on various registries. */\nexport interface DockerRegistryAccount {\n\t/**\n\t * The Mongo ID of the docker registry account.\n\t * This field is de/serialized from/to JSON as\n\t * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of DockerRegistryAccount) }`\n\t */\n\t_id?: MongoId;\n\t/**\n\t * The domain of the provider.\n\t * \n\t * For docker registry, this can include 'http://...',\n\t * however this is not recommended and won't work unless \"insecure registries\" are enabled\n\t * on your hosts. See <https://docs.docker.com/reference/cli/dockerd/#insecure-registries>.\n\t */\n\tdomain: string;\n\t/** The account username */\n\tusername?: string;\n\t/**\n\t * The token in plain text on the db.\n\t * If the database / host can be accessed this is insecure.\n\t */\n\ttoken?: string;\n}\n\nexport type CreateDockerRegistryAccountResponse = DockerRegistryAccount;\n\n/**\n * Configuration to access private git repos from various git providers.\n * Note. Cannot create two accounts with the same domain and username.\n */\nexport interface GitProviderAccount {\n\t/**\n\t * The Mongo ID of the git provider account.\n\t * This field is de/serialized from/to JSON as\n\t * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized User) }`\n\t */\n\t_id?: MongoId;\n\t/**\n\t * The domain of the provider.\n\t * \n\t * For git, this cannot include the protocol eg 'http://',\n\t * which is controlled with 'https' field.\n\t */\n\tdomain: string;\n\t/** Whether git provider is accessed over http or https. */\n\thttps: boolean;\n\t/** The account username */\n\tusername?: string;\n\t/**\n\t * The token in plain text on the db.\n\t * If the database / host can be accessed this is insecure.\n\t */\n\ttoken?: string;\n}\n\nexport type CreateGitProviderAccountResponse = GitProviderAccount;\n\nexport type UserConfig = \n\t/** User that logs in with username / password */\n\t| { type: \"Local\", data: {\n\tpassword: string;\n}}\n\t/** User that logs in via Google Oauth */\n\t| { type: \"Google\", data: {\n\tgoogle_id: string;\n\tavatar: string;\n}}\n\t/** User that logs in via Github Oauth */\n\t| { type: \"Github\", data: {\n\tgithub_id: string;\n\tavatar: string;\n}}\n\t/** User that logs in via Oidc provider */\n\t| { type: \"Oidc\", data: {\n\tprovider: string;\n\tuser_id: string;\n}}\n\t/** Non-human managed user, can have it's own permissions / api keys */\n\t| { type: \"Service\", data: {\n\tdescription: string;\n}};\n\nexport interface User {\n\t/**\n\t * The Mongo ID of the User.\n\t * This field is de/serialized from/to JSON as\n\t * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of User schema) }`\n\t */\n\t_id?: MongoId;\n\t/** The globally unique username for the user. */\n\tusername: string;\n\t/** Whether user is enabled / able to access the api. */\n\tenabled?: boolean;\n\t/** Can give / take other users admin priviledges. */\n\tsuper_admin?: boolean;\n\t/** Whether the user has global admin permissions. */\n\tadmin?: boolean;\n\t/** Whether the user has permission to create servers. */\n\tcreate_server_permissions?: boolean;\n\t/** Whether the user has permission to create builds */\n\tcreate_build_permissions?: boolean;\n\t/** The user-type specific config. */\n\tconfig: UserConfig;\n\t/** When the user last opened updates dropdown. */\n\tlast_update_view?: I64;\n\t/** Recently viewed ids */\n\trecents?: Record<ResourceTarget[\"type\"], string[]>;\n\t/** Give the user elevated permissions on all resources of a certain type */\n\tall?: Record<ResourceTarget[\"type\"], PermissionLevelAndSpecifics | PermissionLevel>;\n\tupdated_at?: I64;\n}\n\nexport type CreateLocalUserResponse = User;\n\nexport type CreateProcedureResponse = Procedure;\n\nexport type CreateRepoWebhookResponse = NoData;\n\nexport type CreateServiceUserResponse = User;\n\nexport type CreateStackWebhookResponse = NoData;\n\nexport type CreateSyncWebhookResponse = NoData;\n\n/**\n * A non-secret global variable which can be interpolated into deployment\n * environment variable values and build argument values.\n */\nexport interface Variable {\n\t/**\n\t * Unique name associated with the variable.\n\t * Instances of '[[variable.name]]' in value will be replaced with 'variable.value'.\n\t */\n\tname: string;\n\t/** A description for the variable. */\n\tdescription?: string;\n\t/** The value associated with the variable. */\n\tvalue?: string;\n\t/**\n\t * If marked as secret, the variable value will be hidden in updates / logs.\n\t * Additionally the value will not be served in read requests by non admin users.\n\t * \n\t * Note that the value is NOT encrypted in the database, and will likely show up in database logs.\n\t * The security of these variables comes down to the security\n\t * of the database (system level encryption, network isolation, etc.)\n\t */\n\tis_secret?: boolean;\n}\n\nexport type CreateVariableResponse = Variable;\n\nexport type DeleteActionWebhookResponse = NoData;\n\nexport type DeleteApiKeyForServiceUserResponse = NoData;\n\nexport type DeleteApiKeyResponse = NoData;\n\nexport type DeleteBuildWebhookResponse = NoData;\n\nexport type DeleteDockerRegistryAccountResponse = DockerRegistryAccount;\n\nexport type DeleteGitProviderAccountResponse = GitProviderAccount;\n\nexport type DeleteProcedureResponse = Procedure;\n\nexport type DeleteRepoWebhookResponse = NoData;\n\nexport type DeleteStackWebhookResponse = NoData;\n\nexport type DeleteSyncWebhookResponse = NoData;\n\nexport type DeleteUserResponse = User;\n\nexport type DeleteVariableResponse = Variable;\n\nexport type DeploymentImage = \n\t/** Deploy any external image. */\n\t| { type: \"Image\", params: {\n\t/** The docker image, can be from any registry that works with docker and that the host server can reach. */\n\timage?: string;\n}}\n\t/** Deploy a Komodo Build. */\n\t| { type: \"Build\", params: {\n\t/** The id of the Build */\n\tbuild_id?: string;\n\t/**\n\t * Use a custom / older version of the image produced by the build.\n\t * if version is 0.0.0, this means `latest` image.\n\t */\n\tversion?: Version;\n}};\n\nexport enum RestartMode {\n\tNoRestart = \"no\",\n\tOnFailure = \"on-failure\",\n\tAlways = \"always\",\n\tUnlessStopped = \"unless-stopped\",\n}\n\nexport enum TerminationSignal {\n\tSigHup = \"SIGHUP\",\n\tSigInt = \"SIGINT\",\n\tSigQuit = \"SIGQUIT\",\n\tSigTerm = \"SIGTERM\",\n}\n\nexport interface DeploymentConfig {\n\t/** The id of server the deployment is deployed on. */\n\tserver_id?: string;\n\t/**\n\t * The image which the deployment deploys.\n\t * Can either be a user inputted image, or a Komodo Build.\n\t */\n\timage?: DeploymentImage;\n\t/**\n\t * Configure the account used to pull the image from the registry.\n\t * Used with `docker login`.\n\t * \n\t * - If the field is empty string, will use the same account config as the build, or none at all if using image.\n\t * - If the field contains an account, a token for the account must be available.\n\t * - Will get the registry domain from the build / image\n\t */\n\timage_registry_account?: string;\n\t/** Whether to skip secret interpolation into the deployment environment variables. */\n\tskip_secret_interp?: boolean;\n\t/** Whether to redeploy the deployment whenever the attached build finishes. */\n\tredeploy_on_build?: boolean;\n\t/** Whether to poll for any updates to the image. */\n\tpoll_for_updates?: boolean;\n\t/**\n\t * Whether to automatically redeploy when\n\t * newer a image is found. Will implicitly\n\t * enable `poll_for_updates`, you don't need to\n\t * enable both.\n\t */\n\tauto_update?: boolean;\n\t/** Whether to send ContainerStateChange alerts for this deployment. */\n\tsend_alerts: boolean;\n\t/** Configure quick links that are displayed in the resource header */\n\tlinks?: string[];\n\t/**\n\t * The network attached to the container.\n\t * Default is `host`.\n\t */\n\tnetwork: string;\n\t/** The restart mode given to the container. */\n\trestart?: RestartMode;\n\t/**\n\t * This is interpolated at the end of the `docker run` command,\n\t * which means they are either passed to the containers inner process,\n\t * or replaces the container command, depending on use of ENTRYPOINT or CMD in dockerfile.\n\t * Empty is no command.\n\t */\n\tcommand?: string;\n\t/** The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal). */\n\ttermination_signal?: TerminationSignal;\n\t/** The termination timeout. */\n\ttermination_timeout: number;\n\t/**\n\t * Extra args which are interpolated into the `docker run` command,\n\t * and affect the container configuration.\n\t */\n\textra_args?: string[];\n\t/**\n\t * Labels attached to various termination signal options.\n\t * Used to specify different shutdown functionality depending on the termination signal.\n\t */\n\tterm_signal_labels?: string;\n\t/**\n\t * The container port mapping.\n\t * Irrelevant if container network is `host`.\n\t * Maps ports on host to ports on container.\n\t */\n\tports?: string;\n\t/**\n\t * The container volume mapping.\n\t * Maps files / folders on host to files / folders in container.\n\t */\n\tvolumes?: string;\n\t/** The environment variables passed to the container. */\n\tenvironment?: string;\n\t/** The docker labels given to the container. */\n\tlabels?: string;\n}\n\nexport type Deployment = Resource<DeploymentConfig, undefined>;\n\n/**\n * Variants de/serialized from/to snake_case.\n * \n * Eg.\n * - NotDeployed -> not_deployed\n * - Restarting -> restarting\n * - Running -> running.\n */\nexport enum DeploymentState {\n\t/** The deployment is currently re/deploying */\n\tDeploying = \"deploying\",\n\t/** Container is running */\n\tRunning = \"running\",\n\t/** Container is created but not running */\n\tCreated = \"created\",\n\t/** Container is in restart loop */\n\tRestarting = \"restarting\",\n\t/** Container is being removed */\n\tRemoving = \"removing\",\n\t/** Container is paused */\n\tPaused = \"paused\",\n\t/** Container is exited */\n\tExited = \"exited\",\n\t/** Container is dead */\n\tDead = \"dead\",\n\t/** The deployment is not deployed (no matching container) */\n\tNotDeployed = \"not_deployed\",\n\t/** Server not reachable for status */\n\tUnknown = \"unknown\",\n}\n\nexport interface DeploymentListItemInfo {\n\t/** The state of the deployment / underlying docker container. */\n\tstate: DeploymentState;\n\t/** The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) */\n\tstatus?: string;\n\t/** The image attached to the deployment. */\n\timage: string;\n\t/** Whether there is a newer image available at the same tag. */\n\tupdate_available: boolean;\n\t/** The server that deployment sits on. */\n\tserver_id: string;\n\t/** An attached Komodo Build, if it exists. */\n\tbuild_id?: string;\n}\n\nexport type DeploymentListItem = ResourceListItem<DeploymentListItemInfo>;\n\nexport interface DeploymentQuerySpecifics {\n\t/**\n\t * Query only for Deployments on these Servers.\n\t * If empty, does not filter by Server.\n\t * Only accepts Server id (not name).\n\t */\n\tserver_ids?: string[];\n\t/**\n\t * Query only for Deployments with these Builds attached.\n\t * If empty, does not filter by Build.\n\t * Only accepts Build id (not name).\n\t */\n\tbuild_ids?: string[];\n\t/** Query only for Deployments with available image updates. */\n\tupdate_available?: boolean;\n}\n\nexport type DeploymentQuery = ResourceQuery<DeploymentQuerySpecifics>;\n\n/** JSON containing an authentication token. */\nexport interface JwtResponse {\n\t/** User ID for signed in user. */\n\tuser_id: string;\n\t/** A token the user can use to authenticate their requests. */\n\tjwt: string;\n}\n\n/** Response for [ExchangeForJwt]. */\nexport type ExchangeForJwtResponse = JwtResponse;\n\n/** Response containing pretty formatted toml contents. */\nexport interface TomlResponse {\n\ttoml: string;\n}\n\nexport type ExportAllResourcesToTomlResponse = TomlResponse;\n\nexport type ExportResourcesToTomlResponse = TomlResponse;\n\nexport type FindUserResponse = User;\n\nexport interface ActionActionState {\n\t/** Number of instances of the Action currently running */\n\trunning: number;\n}\n\nexport type GetActionActionStateResponse = ActionActionState;\n\nexport type GetActionResponse = Action;\n\n/** Severity level of problem. */\nexport enum SeverityLevel {\n\t/**\n\t * No problem.\n\t * \n\t * Aliases: ok, low, l\n\t */\n\tOk = \"OK\",\n\t/**\n\t * Problem is imminent.\n\t * \n\t * Aliases: warning, w, medium, m\n\t */\n\tWarning = \"WARNING\",\n\t/**\n\t * Problem fully realized.\n\t * \n\t * Aliases: critical, c, high, h\n\t */\n\tCritical = \"CRITICAL\",\n}\n\n/** The variants of data related to the alert. */\nexport type AlertData = \n\t/** A null alert */\n\t| { type: \"None\", data: {\n}}\n\t/**\n\t * The user triggered a test of the\n\t * Alerter configuration.\n\t */\n\t| { type: \"Test\", data: {\n\t/** The id of the alerter */\n\tid: string;\n\t/** The name of the alerter */\n\tname: string;\n}}\n\t/** A server could not be reached. */\n\t| { type: \"ServerUnreachable\", data: {\n\t/** The id of the server */\n\tid: string;\n\t/** The name of the server */\n\tname: string;\n\t/** The region of the server */\n\tregion?: string;\n\t/** The error data */\n\terr?: _Serror;\n}}\n\t/** A server has high CPU usage. */\n\t| { type: \"ServerCpu\", data: {\n\t/** The id of the server */\n\tid: string;\n\t/** The name of the server */\n\tname: string;\n\t/** The region of the server */\n\tregion?: string;\n\t/** The cpu usage percentage */\n\tpercentage: number;\n}}\n\t/** A server has high memory usage. */\n\t| { type: \"ServerMem\", data: {\n\t/** The id of the server */\n\tid: string;\n\t/** The name of the server */\n\tname: string;\n\t/** The region of the server */\n\tregion?: string;\n\t/** The used memory */\n\tused_gb: number;\n\t/** The total memory */\n\ttotal_gb: number;\n}}\n\t/** A server has high disk usage. */\n\t| { type: \"ServerDisk\", data: {\n\t/** The id of the server */\n\tid: string;\n\t/** The name of the server */\n\tname: string;\n\t/** The region of the server */\n\tregion?: string;\n\t/** The mount path of the disk */\n\tpath: string;\n\t/** The used portion of the disk in GB */\n\tused_gb: number;\n\t/** The total size of the disk in GB */\n\ttotal_gb: number;\n}}\n\t/** A server has a version mismatch with the core. */\n\t| { type: \"ServerVersionMismatch\", data: {\n\t/** The id of the server */\n\tid: string;\n\t/** The name of the server */\n\tname: string;\n\t/** The region of the server */\n\tregion?: string;\n\t/** The actual server version */\n\tserver_version: string;\n\t/** The core version */\n\tcore_version: string;\n}}\n\t/** A container's state has changed unexpectedly. */\n\t| { type: \"ContainerStateChange\", data: {\n\t/** The id of the deployment */\n\tid: string;\n\t/** The name of the deployment */\n\tname: string;\n\t/** The server id of server that the deployment is on */\n\tserver_id: string;\n\t/** The server name */\n\tserver_name: string;\n\t/** The previous container state */\n\tfrom: DeploymentState;\n\t/** The current container state */\n\tto: DeploymentState;\n}}\n\t/** A Deployment has an image update available */\n\t| { type: \"DeploymentImageUpdateAvailable\", data: {\n\t/** The id of the deployment */\n\tid: string;\n\t/** The name of the deployment */\n\tname: string;\n\t/** The server id of server that the deployment is on */\n\tserver_id: string;\n\t/** The server name */\n\tserver_name: string;\n\t/** The image with update */\n\timage: string;\n}}\n\t/** A Deployment has an image update available */\n\t| { type: \"DeploymentAutoUpdated\", data: {\n\t/** The id of the deployment */\n\tid: string;\n\t/** The name of the deployment */\n\tname: string;\n\t/** The server id of server that the deployment is on */\n\tserver_id: string;\n\t/** The server name */\n\tserver_name: string;\n\t/** The updated image */\n\timage: string;\n}}\n\t/** A stack's state has changed unexpectedly. */\n\t| { type: \"StackStateChange\", data: {\n\t/** The id of the stack */\n\tid: string;\n\t/** The name of the stack */\n\tname: string;\n\t/** The server id of server that the stack is on */\n\tserver_id: string;\n\t/** The server name */\n\tserver_name: string;\n\t/** The previous stack state */\n\tfrom: StackState;\n\t/** The current stack state */\n\tto: StackState;\n}}\n\t/** A Stack has an image update available */\n\t| { type: \"StackImageUpdateAvailable\", data: {\n\t/** The id of the stack */\n\tid: string;\n\t/** The name of the stack */\n\tname: string;\n\t/** The server id of server that the stack is on */\n\tserver_id: string;\n\t/** The server name */\n\tserver_name: string;\n\t/** The service name to update */\n\tservice: string;\n\t/** The image with update */\n\timage: string;\n}}\n\t/** A Stack was auto updated */\n\t| { type: \"StackAutoUpdated\", data: {\n\t/** The id of the stack */\n\tid: string;\n\t/** The name of the stack */\n\tname: string;\n\t/** The server id of server that the stack is on */\n\tserver_id: string;\n\t/** The server name */\n\tserver_name: string;\n\t/** One or more images that were updated */\n\timages: string[];\n}}\n\t/** An AWS builder failed to terminate. */\n\t| { type: \"AwsBuilderTerminationFailed\", data: {\n\t/** The id of the aws instance which failed to terminate */\n\tinstance_id: string;\n\t/** A reason for the failure */\n\tmessage: string;\n}}\n\t/** A resource sync has pending updates */\n\t| { type: \"ResourceSyncPendingUpdates\", data: {\n\t/** The id of the resource sync */\n\tid: string;\n\t/** The name of the resource sync */\n\tname: string;\n}}\n\t/** A build has failed */\n\t| { type: \"BuildFailed\", data: {\n\t/** The id of the build */\n\tid: string;\n\t/** The name of the build */\n\tname: string;\n\t/** The version that failed to build */\n\tversion: Version;\n}}\n\t/** A repo has failed */\n\t| { type: \"RepoBuildFailed\", data: {\n\t/** The id of the repo */\n\tid: string;\n\t/** The name of the repo */\n\tname: string;\n}}\n\t/** A procedure has failed */\n\t| { type: \"ProcedureFailed\", data: {\n\t/** The id of the procedure */\n\tid: string;\n\t/** The name of the procedure */\n\tname: string;\n}}\n\t/** An action has failed */\n\t| { type: \"ActionFailed\", data: {\n\t/** The id of the action */\n\tid: string;\n\t/** The name of the action */\n\tname: string;\n}}\n\t/** A schedule was run */\n\t| { type: \"ScheduleRun\", data: {\n\t/** Procedure or Action */\n\tresource_type: ResourceTarget[\"type\"];\n\t/** The resource id */\n\tid: string;\n\t/** The resource name */\n\tname: string;\n}}\n\t/**\n\t * Custom header / body.\n\t * Produced using `/execute/SendAlert`\n\t */\n\t| { type: \"Custom\", data: {\n\t/** The alert message. */\n\tmessage: string;\n\t/** Message details. May be empty string. */\n\tdetails?: string;\n}};\n\n/** Representation of an alert in the system. */\nexport interface Alert {\n\t/**\n\t * The Mongo ID of the alert.\n\t * This field is de/serialized from/to JSON as\n\t * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Alert) }`\n\t */\n\t_id?: MongoId;\n\t/** Unix timestamp in milliseconds the alert was opened */\n\tts: I64;\n\t/** Whether the alert is already resolved */\n\tresolved: boolean;\n\t/** The severity of the alert */\n\tlevel: SeverityLevel;\n\t/** The target of the alert */\n\ttarget: ResourceTarget;\n\t/** The data attached to the alert */\n\tdata: AlertData;\n\t/** The timestamp of alert resolution */\n\tresolved_ts?: I64;\n}\n\nexport type GetAlertResponse = Alert;\n\nexport type GetAlerterResponse = Alerter;\n\nexport interface BuildActionState {\n\tbuilding: boolean;\n}\n\nexport type GetBuildActionStateResponse = BuildActionState;\n\nexport type GetBuildResponse = Build;\n\nexport type GetBuilderResponse = Builder;\n\nexport type GetContainerLogResponse = Log;\n\nexport interface DeploymentActionState {\n\tpulling: boolean;\n\tdeploying: boolean;\n\tstarting: boolean;\n\trestarting: boolean;\n\tpausing: boolean;\n\tunpausing: boolean;\n\tstopping: boolean;\n\tdestroying: boolean;\n\trenaming: boolean;\n}\n\nexport type GetDeploymentActionStateResponse = DeploymentActionState;\n\nexport type GetDeploymentLogResponse = Log;\n\nexport type GetDeploymentResponse = Deployment;\n\nexport interface ContainerStats {\n\tname: string;\n\tcpu_perc: string;\n\tmem_perc: string;\n\tmem_usage: string;\n\tnet_io: string;\n\tblock_io: string;\n\tpids: string;\n}\n\nexport type GetDeploymentStatsResponse = ContainerStats;\n\nexport type GetDockerRegistryAccountResponse = DockerRegistryAccount;\n\nexport type GetGitProviderAccountResponse = GitProviderAccount;\n\nexport type GetPermissionResponse = PermissionLevelAndSpecifics;\n\nexport interface ProcedureActionState {\n\trunning: boolean;\n}\n\nexport type GetProcedureActionStateResponse = ProcedureActionState;\n\nexport type GetProcedureResponse = Procedure;\n\nexport interface RepoActionState {\n\t/** Whether Repo currently cloning on the attached Server */\n\tcloning: boolean;\n\t/** Whether Repo currently pulling on the attached Server */\n\tpulling: boolean;\n\t/** Whether Repo currently building using the attached Builder. */\n\tbuilding: boolean;\n\t/** Whether Repo currently renaming. */\n\trenaming: boolean;\n}\n\nexport type GetRepoActionStateResponse = RepoActionState;\n\nexport interface RepoConfig {\n\t/** The server to clone the repo on. */\n\tserver_id?: string;\n\t/** Attach a builder to 'build' the repo. */\n\tbuilder_id?: string;\n\t/** The git provider domain. Default: github.com */\n\tgit_provider: string;\n\t/**\n\t * Whether to use https to clone the repo (versus http). Default: true\n\t * \n\t * Note. Komodo does not currently support cloning repos via ssh.\n\t */\n\tgit_https: boolean;\n\t/**\n\t * The git account used to access private repos.\n\t * Passing empty string can only clone public repos.\n\t * \n\t * Note. A token for the account must be available in the core config or the builder server's periphery config\n\t * for the configured git provider.\n\t */\n\tgit_account?: string;\n\t/** The github repo to clone. */\n\trepo?: string;\n\t/** The repo branch. */\n\tbranch: string;\n\t/** Optionally set a specific commit hash. */\n\tcommit?: string;\n\t/**\n\t * Explicitly specify the folder to clone the repo in.\n\t * - If absolute (has leading '/')\n\t * - Used directly as the path\n\t * - If relative\n\t * - Taken relative to Periphery `repo_dir` (ie `${root_directory}/repos`)\n\t */\n\tpath?: string;\n\t/** Whether incoming webhooks actually trigger action. */\n\twebhook_enabled: boolean;\n\t/**\n\t * Optionally provide an alternate webhook secret for this repo.\n\t * If its an empty string, use the default secret from the config.\n\t */\n\twebhook_secret?: string;\n\t/**\n\t * Command to be run after the repo is cloned.\n\t * The path is relative to the root of the repo.\n\t */\n\ton_clone?: SystemCommand;\n\t/**\n\t * Command to be run after the repo is pulled.\n\t * The path is relative to the root of the repo.\n\t */\n\ton_pull?: SystemCommand;\n\t/** Configure quick links that are displayed in the resource header */\n\tlinks?: string[];\n\t/**\n\t * The environment variables passed to the compose file.\n\t * They will be written to path defined in env_file_path,\n\t * which is given relative to the run directory.\n\t * \n\t * If it is empty, no file will be written.\n\t */\n\tenvironment?: string;\n\t/**\n\t * The name of the written environment file before `docker compose up`.\n\t * Relative to the repo root.\n\t * Default: .env\n\t */\n\tenv_file_path: string;\n\t/** Whether to skip secret interpolation into the repo environment variable file. */\n\tskip_secret_interp?: boolean;\n}\n\nexport interface RepoInfo {\n\t/** When repo was last pulled */\n\tlast_pulled_at?: I64;\n\t/** When repo was last built */\n\tlast_built_at?: I64;\n\t/** Latest built short commit hash, or null. */\n\tbuilt_hash?: string;\n\t/** Latest built commit message, or null. Only for repo based stacks */\n\tbuilt_message?: string;\n\t/** Latest remote short commit hash, or null. */\n\tlatest_hash?: string;\n\t/** Latest remote commit message, or null */\n\tlatest_message?: string;\n}\n\nexport type Repo = Resource<RepoConfig, RepoInfo>;\n\nexport type GetRepoResponse = Repo;\n\nexport interface ResourceSyncActionState {\n\t/** Whether sync currently syncing */\n\tsyncing: boolean;\n}\n\nexport type GetResourceSyncActionStateResponse = ResourceSyncActionState;\n\n/** The sync configuration. */\nexport interface ResourceSyncConfig {\n\t/** Choose a Komodo Repo (Resource) to source the sync files. */\n\tlinked_repo?: string;\n\t/** The git provider domain. Default: github.com */\n\tgit_provider: string;\n\t/**\n\t * Whether to use https to clone the repo (versus http). Default: true\n\t * \n\t * Note. Komodo does not currently support cloning repos via ssh.\n\t */\n\tgit_https: boolean;\n\t/** The Github repo used as the source of the build. */\n\trepo?: string;\n\t/** The branch of the repo. */\n\tbranch: string;\n\t/** Optionally set a specific commit hash. */\n\tcommit?: string;\n\t/**\n\t * The git account used to access private repos.\n\t * Passing empty string can only clone public repos.\n\t * \n\t * Note. A token for the account must be available in the core config or the builder server's periphery config\n\t * for the configured git provider.\n\t */\n\tgit_account?: string;\n\t/** Whether incoming webhooks actually trigger action. */\n\twebhook_enabled: boolean;\n\t/**\n\t * Optionally provide an alternate webhook secret for this sync.\n\t * If its an empty string, use the default secret from the config.\n\t */\n\twebhook_secret?: string;\n\t/**\n\t * Files are available on the Komodo Core host.\n\t * Specify the file / folder with [ResourceSyncConfig::resource_path].\n\t */\n\tfiles_on_host?: boolean;\n\t/**\n\t * The path of the resource file(s) to sync.\n\t * - If Files on Host, this is relative to the configured `sync_directory` in core config.\n\t * - If Git Repo based, this is relative to the root of the repo.\n\t * Can be a specific file, or a directory containing multiple files / folders.\n\t * See [https://komo.do/docs/sync-resources](https://komo.do/docs/sync-resources) for more information.\n\t */\n\tresource_path?: string[];\n\t/**\n\t * Enable \"pushes\" to the file,\n\t * which exports resources matching tags to single file.\n\t * - If using `files_on_host`, it is stored in the file_contents, which must point to a .toml file path (it will be created if it doesn't exist).\n\t * - If using `file_contents`, it is stored in the database.\n\t * When using this, \"delete\" mode is always enabled.\n\t */\n\tmanaged?: boolean;\n\t/**\n\t * Whether sync should delete resources\n\t * not declared in the resource files\n\t */\n\tdelete?: boolean;\n\t/**\n\t * Whether sync should include resources.\n\t * Default: true\n\t */\n\tinclude_resources: boolean;\n\t/**\n\t * When using `managed` resource sync, will only export resources\n\t * matching all of the given tags. If none, will match all resources.\n\t */\n\tmatch_tags?: string[];\n\t/** Whether sync should include variables. */\n\tinclude_variables?: boolean;\n\t/** Whether sync should include user groups. */\n\tinclude_user_groups?: boolean;\n\t/**\n\t * Whether sync should send alert when it enters Pending state.\n\t * Default: true\n\t */\n\tpending_alert: boolean;\n\t/** Manage the file contents in the UI. */\n\tfile_contents?: string;\n}\n\nexport type DiffData = \n\t/** Resource will be created */\n\t| { type: \"Create\", data: {\n\t/** The name of resource to create */\n\tname?: string;\n\t/** The proposed resource to create in TOML */\n\tproposed: string;\n}}\n\t| { type: \"Update\", data: {\n\t/** The proposed TOML */\n\tproposed: string;\n\t/** The current TOML */\n\tcurrent: string;\n}}\n\t| { type: \"Delete\", data: {\n\t/** The current TOML of the resource to delete */\n\tcurrent: string;\n}};\n\nexport interface ResourceDiff {\n\t/**\n\t * The resource target.\n\t * The target id will be empty if \"Create\" ResourceDiffType.\n\t */\n\ttarget: ResourceTarget;\n\t/** The data associated with the diff. */\n\tdata: DiffData;\n}\n\nexport interface SyncDeployUpdate {\n\t/** Resources to deploy */\n\tto_deploy: number;\n\t/** A readable log of all the changes to be applied */\n\tlog: string;\n}\n\nexport interface SyncFileContents {\n\t/** The base resource path. */\n\tresource_path?: string;\n\t/** The path of the file / error path relative to the resource path. */\n\tpath: string;\n\t/** The contents of the file */\n\tcontents: string;\n}\n\nexport interface ResourceSyncInfo {\n\t/** Unix timestamp of last applied sync */\n\tlast_sync_ts?: I64;\n\t/** Short commit hash of last applied sync */\n\tlast_sync_hash?: string;\n\t/** Commit message of last applied sync */\n\tlast_sync_message?: string;\n\t/** The list of pending updates to resources */\n\tresource_updates?: ResourceDiff[];\n\t/** The list of pending updates to variables */\n\tvariable_updates?: DiffData[];\n\t/** The list of pending updates to user groups */\n\tuser_group_updates?: DiffData[];\n\t/** The list of pending deploys to resources. */\n\tpending_deploy?: SyncDeployUpdate;\n\t/** If there is an error, it will be stored here */\n\tpending_error?: string;\n\t/** The commit hash which produced these pending updates. */\n\tpending_hash?: string;\n\t/** The commit message which produced these pending updates. */\n\tpending_message?: string;\n\t/** The current sync files */\n\tremote_contents?: SyncFileContents[];\n\t/** Any read errors in files by path */\n\tremote_errors?: SyncFileContents[];\n}\n\nexport type ResourceSync = Resource<ResourceSyncConfig, ResourceSyncInfo>;\n\nexport type GetResourceSyncResponse = ResourceSync;\n\n/** Current pending actions on the server. */\nexport interface ServerActionState {\n\t/** Server currently pruning networks */\n\tpruning_networks: boolean;\n\t/** Server currently pruning containers */\n\tpruning_containers: boolean;\n\t/** Server currently pruning images */\n\tpruning_images: boolean;\n\t/** Server currently pruning volumes */\n\tpruning_volumes: boolean;\n\t/** Server currently pruning docker builders */\n\tpruning_builders: boolean;\n\t/** Server currently pruning builx cache */\n\tpruning_buildx: boolean;\n\t/** Server currently pruning system */\n\tpruning_system: boolean;\n\t/** Server currently starting containers. */\n\tstarting_containers: boolean;\n\t/** Server currently restarting containers. */\n\trestarting_containers: boolean;\n\t/** Server currently pausing containers. */\n\tpausing_containers: boolean;\n\t/** Server currently unpausing containers. */\n\tunpausing_containers: boolean;\n\t/** Server currently stopping containers. */\n\tstopping_containers: boolean;\n}\n\nexport type GetServerActionStateResponse = ServerActionState;\n\n/** Server configuration. */\nexport interface ServerConfig {\n\t/**\n\t * The http address of the periphery client.\n\t * Default: http://localhost:8120\n\t */\n\taddress: string;\n\t/**\n\t * The address to use with links for containers on the server.\n\t * If empty, will use the 'address' for links.\n\t */\n\texternal_address?: string;\n\t/** An optional region label */\n\tregion?: string;\n\t/**\n\t * Whether a server is enabled.\n\t * If a server is disabled,\n\t * you won't be able to perform any actions on it or see deployment's status.\n\t * Default: false\n\t */\n\tenabled: boolean;\n\t/**\n\t * The timeout used to reach the server in seconds.\n\t * default: 2\n\t */\n\ttimeout_seconds: I64;\n\t/**\n\t * An optional override passkey to use\n\t * to authenticate with periphery agent.\n\t * If this is empty, will use passkey in core config.\n\t */\n\tpasskey?: string;\n\t/**\n\t * Sometimes the system stats reports a mount path that is not desired.\n\t * Use this field to filter it out from the report.\n\t */\n\tignore_mounts?: string[];\n\t/**\n\t * Whether to monitor any server stats beyond passing health check.\n\t * default: true\n\t */\n\tstats_monitoring: boolean;\n\t/**\n\t * Whether to trigger 'docker image prune -a -f' every 24 hours.\n\t * default: true\n\t */\n\tauto_prune: boolean;\n\t/** Configure quick links that are displayed in the resource header */\n\tlinks?: string[];\n\t/** Whether to send alerts about the servers reachability */\n\tsend_unreachable_alerts: boolean;\n\t/** Whether to send alerts about the servers CPU status */\n\tsend_cpu_alerts: boolean;\n\t/** Whether to send alerts about the servers MEM status */\n\tsend_mem_alerts: boolean;\n\t/** Whether to send alerts about the servers DISK status */\n\tsend_disk_alerts: boolean;\n\t/** Whether to send alerts about the servers version mismatch with core */\n\tsend_version_mismatch_alerts: boolean;\n\t/** The percentage threshhold which triggers WARNING state for CPU. */\n\tcpu_warning: number;\n\t/** The percentage threshhold which triggers CRITICAL state for CPU. */\n\tcpu_critical: number;\n\t/** The percentage threshhold which triggers WARNING state for MEM. */\n\tmem_warning: number;\n\t/** The percentage threshhold which triggers CRITICAL state for MEM. */\n\tmem_critical: number;\n\t/** The percentage threshhold which triggers WARNING state for DISK. */\n\tdisk_warning: number;\n\t/** The percentage threshhold which triggers CRITICAL state for DISK. */\n\tdisk_critical: number;\n\t/** Scheduled maintenance windows during which alerts will be suppressed. */\n\tmaintenance_windows?: MaintenanceWindow[];\n}\n\nexport type Server = Resource<ServerConfig, undefined>;\n\nexport type GetServerResponse = Server;\n\nexport interface StackActionState {\n\tpulling: boolean;\n\tdeploying: boolean;\n\tstarting: boolean;\n\trestarting: boolean;\n\tpausing: boolean;\n\tunpausing: boolean;\n\tstopping: boolean;\n\tdestroying: boolean;\n}\n\nexport type GetStackActionStateResponse = StackActionState;\n\nexport type GetStackLogResponse = Log;\n\nexport enum StackFileRequires {\n\t/** Diff requires service redeploy. */\n\tRedeploy = \"Redeploy\",\n\t/** Diff requires service restart */\n\tRestart = \"Restart\",\n\t/** Diff requires no action. Default. */\n\tNone = \"None\",\n}\n\n/** Configure additional file dependencies of the Stack. */\nexport interface StackFileDependency {\n\t/** Specify the file */\n\tpath: string;\n\t/** Specify specific service/s */\n\tservices?: string[];\n\t/** Specify */\n\trequires?: StackFileRequires;\n}\n\n/** The compose file configuration. */\nexport interface StackConfig {\n\t/** The server to deploy the stack on. */\n\tserver_id?: string;\n\t/** Configure quick links that are displayed in the resource header */\n\tlinks?: string[];\n\t/**\n\t * Optionally specify a custom project name for the stack.\n\t * If this is empty string, it will default to the stack name.\n\t * Used with `docker compose -p {project_name}`.\n\t * \n\t * Note. Can be used to import pre-existing stacks.\n\t */\n\tproject_name?: string;\n\t/**\n\t * Whether to automatically `compose pull` before redeploying stack.\n\t * Ensured latest images are deployed.\n\t * Will fail if the compose file specifies a locally build image.\n\t */\n\tauto_pull: boolean;\n\t/**\n\t * Whether to `docker compose build` before `compose down` / `compose up`.\n\t * Combine with build_extra_args for custom behaviors.\n\t */\n\trun_build?: boolean;\n\t/** Whether to poll for any updates to the images. */\n\tpoll_for_updates?: boolean;\n\t/**\n\t * Whether to automatically redeploy when\n\t * newer images are found. Will implicitly\n\t * enable `poll_for_updates`, you don't need to\n\t * enable both.\n\t */\n\tauto_update?: boolean;\n\t/**\n\t * If auto update is enabled, Komodo will\n\t * by default only update the specific services\n\t * with image updates. If this parameter is set to true,\n\t * Komodo will redeploy the whole Stack (all services).\n\t */\n\tauto_update_all_services?: boolean;\n\t/** Whether to run `docker compose down` before `compose up`. */\n\tdestroy_before_deploy?: boolean;\n\t/** Whether to skip secret interpolation into the stack environment variables. */\n\tskip_secret_interp?: boolean;\n\t/** Choose a Komodo Repo (Resource) to source the compose files. */\n\tlinked_repo?: string;\n\t/** The git provider domain. Default: github.com */\n\tgit_provider: string;\n\t/**\n\t * Whether to use https to clone the repo (versus http). Default: true\n\t * \n\t * Note. Komodo does not currently support cloning repos via ssh.\n\t */\n\tgit_https: boolean;\n\t/**\n\t * The git account used to access private repos.\n\t * Passing empty string can only clone public repos.\n\t * \n\t * Note. A token for the account must be available in the core config or the builder server's periphery config\n\t * for the configured git provider.\n\t */\n\tgit_account?: string;\n\t/**\n\t * The repo used as the source of the build.\n\t * {namespace}/{repo_name}\n\t */\n\trepo?: string;\n\t/** The branch of the repo. */\n\tbranch: string;\n\t/** Optionally set a specific commit hash. */\n\tcommit?: string;\n\t/** Optionally set a specific clone path */\n\tclone_path?: string;\n\t/**\n\t * By default, the Stack will `git pull` the repo after it is first cloned.\n\t * If this option is enabled, the repo folder will be deleted and recloned instead.\n\t */\n\treclone?: boolean;\n\t/** Whether incoming webhooks actually trigger action. */\n\twebhook_enabled: boolean;\n\t/**\n\t * Optionally provide an alternate webhook secret for this stack.\n\t * If its an empty string, use the default secret from the config.\n\t */\n\twebhook_secret?: string;\n\t/**\n\t * By default, the Stack will `DeployStackIfChanged`.\n\t * If this option is enabled, will always run `DeployStack` without diffing.\n\t */\n\twebhook_force_deploy?: boolean;\n\t/**\n\t * If this is checked, the stack will source the files on the host.\n\t * Use `run_directory` and `file_paths` to specify the path on the host.\n\t * This is useful for those who wish to setup their files on the host,\n\t * rather than defining the contents in UI or in a git repo.\n\t */\n\tfiles_on_host?: boolean;\n\t/** Directory to change to (`cd`) before running `docker compose up -d`. */\n\trun_directory?: string;\n\t/**\n\t * Add paths to compose files, relative to the run path.\n\t * If this is empty, will use file `compose.yaml`.\n\t */\n\tfile_paths?: string[];\n\t/**\n\t * The name of the written environment file before `docker compose up`.\n\t * Relative to the run directory root.\n\t * Default: .env\n\t */\n\tenv_file_path: string;\n\t/**\n\t * Add additional env files to attach with `--env-file`.\n\t * Relative to the run directory root.\n\t * \n\t * Note. It is already included as an `additional_file`.\n\t * Don't add it again there.\n\t */\n\tadditional_env_files?: string[];\n\t/**\n\t * Add additional config files either in repo or on host to track.\n\t * Can add any files associated with the stack to enable editing them in the UI.\n\t * Doing so will also include diffing these when deciding to deploy in `DeployStackIfChanged`.\n\t * Relative to the run directory.\n\t * \n\t * Note. If the config file is .env and should be included in compose command\n\t * using `--env-file`, add it to `additional_env_files` instead.\n\t */\n\tconfig_files?: StackFileDependency[];\n\t/** Whether to send StackStateChange alerts for this stack. */\n\tsend_alerts: boolean;\n\t/** Used with `registry_account` to login to a registry before docker compose up. */\n\tregistry_provider?: string;\n\t/** Used with `registry_provider` to login to a registry before docker compose up. */\n\tregistry_account?: string;\n\t/** The optional command to run before the Stack is deployed. */\n\tpre_deploy?: SystemCommand;\n\t/** The optional command to run after the Stack is deployed. */\n\tpost_deploy?: SystemCommand;\n\t/**\n\t * The extra arguments to pass after `docker compose up -d`.\n\t * If empty, no extra arguments will be passed.\n\t */\n\textra_args?: string[];\n\t/**\n\t * The extra arguments to pass after `docker compose build`.\n\t * If empty, no extra build arguments will be passed.\n\t * Only used if `run_build: true`\n\t */\n\tbuild_extra_args?: string[];\n\t/**\n\t * Ignore certain services declared in the compose file when checking\n\t * the stack status. For example, an init service might be exited, but the\n\t * stack should be healthy. This init service should be in `ignore_services`\n\t */\n\tignore_services?: string[];\n\t/**\n\t * The contents of the file directly, for management in the UI.\n\t * If this is empty, it will fall back to checking git config for\n\t * repo based compose file.\n\t * Supports variable / secret interpolation.\n\t */\n\tfile_contents?: string;\n\t/**\n\t * The environment variables passed to the compose file.\n\t * They will be written to path defined in env_file_path,\n\t * which is given relative to the run directory.\n\t * \n\t * If it is empty, no file will be written.\n\t */\n\tenvironment?: string;\n}\n\nexport interface FileContents {\n\t/** The path to the file */\n\tpath: string;\n\t/** The contents of the file */\n\tcontents: string;\n}\n\nexport interface StackServiceNames {\n\t/** The name of the service */\n\tservice_name: string;\n\t/**\n\t * Will either be the declared container_name in the compose file,\n\t * or a pattern to match auto named containers.\n\t * \n\t * Auto named containers are composed of three parts:\n\t * \n\t * 1. The name of the compose project (top level name field of compose file).\n\t * This defaults to the name of the parent folder of the compose file.\n\t * Komodo will always set it to be the name of the stack, but imported stacks\n\t * will have a different name.\n\t * 2. The service name\n\t * 3. The replica number\n\t * \n\t * Example: stacko-mongo-1.\n\t * \n\t * This stores only 1. and 2., ie stacko-mongo.\n\t * Containers will be matched via regex like `^container_name-?[0-9]*$``\n\t */\n\tcontainer_name: string;\n\t/** The services image. */\n\timage?: string;\n}\n\n/**\n * Same as [FileContents] with some extra\n * info specific to Stacks.\n */\nexport interface StackRemoteFileContents {\n\t/** The path to the file */\n\tpath: string;\n\t/** The contents of the file */\n\tcontents: string;\n\t/**\n\t * The services depending on this file,\n\t * or empty for global requirement (eg all compose files and env files).\n\t */\n\tservices?: string[];\n\t/** Whether diff requires Redeploy / Restart / None */\n\trequires?: StackFileRequires;\n}\n\nexport interface StackInfo {\n\t/**\n\t * If any of the expected compose / additional files are missing in the repo,\n\t * they will be stored here.\n\t */\n\tmissing_files?: string[];\n\t/**\n\t * The deployed project name.\n\t * This is updated whenever Komodo successfully deploys the stack.\n\t * If it is present, Komodo will use it for actions over other options,\n\t * to ensure control is maintained after changing the project name (there is no rename compose project api).\n\t */\n\tdeployed_project_name?: string;\n\t/** Deployed short commit hash, or null. Only for repo based stacks. */\n\tdeployed_hash?: string;\n\t/** Deployed commit message, or null. Only for repo based stacks */\n\tdeployed_message?: string;\n\t/**\n\t * The deployed compose / additional file contents.\n\t * This is updated whenever Komodo successfully deploys the stack.\n\t */\n\tdeployed_contents?: FileContents[];\n\t/**\n\t * The deployed service names.\n\t * This is updated whenever it is empty, or deployed contents is updated.\n\t */\n\tdeployed_services?: StackServiceNames[];\n\t/**\n\t * The output of `docker compose config`.\n\t * This is updated whenever Komodo successfully deploys the stack.\n\t */\n\tdeployed_config?: string;\n\t/**\n\t * The latest service names.\n\t * This is updated whenever the stack cache refreshes, using the latest file contents (either db defined or remote).\n\t */\n\tlatest_services?: StackServiceNames[];\n\t/**\n\t * The remote compose / additional file contents, whether on host or in repo.\n\t * This is updated whenever Komodo refreshes the stack cache.\n\t * It will be empty if the file is defined directly in the stack config.\n\t */\n\tremote_contents?: StackRemoteFileContents[];\n\t/** If there was an error in getting the remote contents, it will be here. */\n\tremote_errors?: FileContents[];\n\t/** Latest commit hash, or null */\n\tlatest_hash?: string;\n\t/** Latest commit message, or null */\n\tlatest_message?: string;\n}\n\nexport type Stack = Resource<StackConfig, StackInfo>;\n\nexport type GetStackResponse = Stack;\n\n/** System information of a server */\nexport interface SystemInformation {\n\t/** The system name */\n\tname?: string;\n\t/** The system long os version */\n\tos?: string;\n\t/** System's kernel version */\n\tkernel?: string;\n\t/** Physical core count */\n\tcore_count?: number;\n\t/** System hostname based off DNS */\n\thost_name?: string;\n\t/** The CPU's brand */\n\tcpu_brand: string;\n\t/** Whether terminals are disabled on this Periphery server */\n\tterminals_disabled: boolean;\n\t/** Whether container exec is disabled on this Periphery server */\n\tcontainer_exec_disabled: boolean;\n}\n\nexport type GetSystemInformationResponse = SystemInformation;\n\nexport interface SystemLoadAverage {\n\t/** 1m load average */\n\tone: number;\n\t/** 5m load average */\n\tfive: number;\n\t/** 15m load average */\n\tfifteen: number;\n}\n\n/** Info for a single disk mounted on the system. */\nexport interface SingleDiskUsage {\n\t/** The mount point of the disk */\n\tmount: string;\n\t/** Detected file system */\n\tfile_system: string;\n\t/** Used portion of the disk in GB */\n\tused_gb: number;\n\t/** Total size of the disk in GB */\n\ttotal_gb: number;\n}\n\nexport enum Timelength {\n\t/** `1-sec` */\n\tOneSecond = \"1-sec\",\n\t/** `5-sec` */\n\tFiveSeconds = \"5-sec\",\n\t/** `10-sec` */\n\tTenSeconds = \"10-sec\",\n\t/** `15-sec` */\n\tFifteenSeconds = \"15-sec\",\n\t/** `30-sec` */\n\tThirtySeconds = \"30-sec\",\n\t/** `1-min` */\n\tOneMinute = \"1-min\",\n\t/** `2-min` */\n\tTwoMinutes = \"2-min\",\n\t/** `5-min` */\n\tFiveMinutes = \"5-min\",\n\t/** `10-min` */\n\tTenMinutes = \"10-min\",\n\t/** `15-min` */\n\tFifteenMinutes = \"15-min\",\n\t/** `30-min` */\n\tThirtyMinutes = \"30-min\",\n\t/** `1-hr` */\n\tOneHour = \"1-hr\",\n\t/** `2-hr` */\n\tTwoHours = \"2-hr\",\n\t/** `6-hr` */\n\tSixHours = \"6-hr\",\n\t/** `8-hr` */\n\tEightHours = \"8-hr\",\n\t/** `12-hr` */\n\tTwelveHours = \"12-hr\",\n\t/** `1-day` */\n\tOneDay = \"1-day\",\n\t/** `3-day` */\n\tThreeDay = \"3-day\",\n\t/** `1-wk` */\n\tOneWeek = \"1-wk\",\n\t/** `2-wk` */\n\tTwoWeeks = \"2-wk\",\n\t/** `30-day` */\n\tThirtyDays = \"30-day\",\n}\n\n/** Realtime system stats data. */\nexport interface SystemStats {\n\t/** Cpu usage percentage */\n\tcpu_perc: number;\n\t/** Load average (1m, 5m, 15m) */\n\tload_average?: SystemLoadAverage;\n\t/**\n\t * [1.15.9+]\n\t * Free memory in GB.\n\t * This is really the 'Free' memory, not the 'Available' memory.\n\t * It may be different than mem_total_gb - mem_used_gb.\n\t */\n\tmem_free_gb?: number;\n\t/** Used memory in GB. 'Total' - 'Available' (not free) memory. */\n\tmem_used_gb: number;\n\t/** Total memory in GB */\n\tmem_total_gb: number;\n\t/** Breakdown of individual disks, ie their usages, sizes, and mount points */\n\tdisks: SingleDiskUsage[];\n\t/** Network ingress usage in MB */\n\tnetwork_ingress_bytes?: number;\n\t/** Network egress usage in MB */\n\tnetwork_egress_bytes?: number;\n\t/** The rate the system stats are being polled from the system */\n\tpolling_rate: Timelength;\n\t/** Unix timestamp in milliseconds when stats were last polled */\n\trefresh_ts: I64;\n\t/** Unix timestamp in milliseconds when disk list was last refreshed */\n\trefresh_list_ts: I64;\n}\n\nexport type GetSystemStatsResponse = SystemStats;\n\nexport enum TagColor {\n\tLightSlate = \"LightSlate\",\n\tSlate = \"Slate\",\n\tDarkSlate = \"DarkSlate\",\n\tLightRed = \"LightRed\",\n\tRed = \"Red\",\n\tDarkRed = \"DarkRed\",\n\tLightOrange = \"LightOrange\",\n\tOrange = \"Orange\",\n\tDarkOrange = \"DarkOrange\",\n\tLightAmber = \"LightAmber\",\n\tAmber = \"Amber\",\n\tDarkAmber = \"DarkAmber\",\n\tLightYellow = \"LightYellow\",\n\tYellow = \"Yellow\",\n\tDarkYellow = \"DarkYellow\",\n\tLightLime = \"LightLime\",\n\tLime = \"Lime\",\n\tDarkLime = \"DarkLime\",\n\tLightGreen = \"LightGreen\",\n\tGreen = \"Green\",\n\tDarkGreen = \"DarkGreen\",\n\tLightEmerald = \"LightEmerald\",\n\tEmerald = \"Emerald\",\n\tDarkEmerald = \"DarkEmerald\",\n\tLightTeal = \"LightTeal\",\n\tTeal = \"Teal\",\n\tDarkTeal = \"DarkTeal\",\n\tLightCyan = \"LightCyan\",\n\tCyan = \"Cyan\",\n\tDarkCyan = \"DarkCyan\",\n\tLightSky = \"LightSky\",\n\tSky = \"Sky\",\n\tDarkSky = \"DarkSky\",\n\tLightBlue = \"LightBlue\",\n\tBlue = \"Blue\",\n\tDarkBlue = \"DarkBlue\",\n\tLightIndigo = \"LightIndigo\",\n\tIndigo = \"Indigo\",\n\tDarkIndigo = \"DarkIndigo\",\n\tLightViolet = \"LightViolet\",\n\tViolet = \"Violet\",\n\tDarkViolet = \"DarkViolet\",\n\tLightPurple = \"LightPurple\",\n\tPurple = \"Purple\",\n\tDarkPurple = \"DarkPurple\",\n\tLightFuchsia = \"LightFuchsia\",\n\tFuchsia = \"Fuchsia\",\n\tDarkFuchsia = \"DarkFuchsia\",\n\tLightPink = \"LightPink\",\n\tPink = \"Pink\",\n\tDarkPink = \"DarkPink\",\n\tLightRose = \"LightRose\",\n\tRose = \"Rose\",\n\tDarkRose = \"DarkRose\",\n}\n\nexport interface Tag {\n\t/**\n\t * The Mongo ID of the tag.\n\t * This field is de/serialized from/to JSON as\n\t * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Tag) }`\n\t */\n\t_id?: MongoId;\n\tname: string;\n\towner?: string;\n\t/** Hex color code with alpha for UI display */\n\tcolor?: TagColor;\n}\n\nexport type GetTagResponse = Tag;\n\nexport type GetUpdateResponse = Update;\n\n/**\n * Permission users at the group level.\n * \n * All users that are part of a group inherit the group's permissions.\n * A user can be a part of multiple groups. A user's permission on a particular resource\n * will be resolved to be the maximum permission level between the user's own permissions and\n * any groups they are a part of.\n */\nexport interface UserGroup {\n\t/**\n\t * The Mongo ID of the UserGroup.\n\t * This field is de/serialized from/to JSON as\n\t * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized User) }`\n\t */\n\t_id?: MongoId;\n\t/** A name for the user group */\n\tname: string;\n\t/** Whether all users will implicitly have the permissions in this group. */\n\teveryone?: boolean;\n\t/** User ids of group members */\n\tusers?: string[];\n\t/** Give the user group elevated permissions on all resources of a certain type */\n\tall?: Record<ResourceTarget[\"type\"], PermissionLevelAndSpecifics | PermissionLevel>;\n\t/** Unix time (ms) when user group last updated */\n\tupdated_at?: I64;\n}\n\nexport type GetUserGroupResponse = UserGroup;\n\nexport type GetUserResponse = User;\n\nexport type GetVariableResponse = Variable;\n\nexport enum ContainerStateStatusEnum {\n\tRunning = \"running\",\n\tCreated = \"created\",\n\tPaused = \"paused\",\n\tRestarting = \"restarting\",\n\tExited = \"exited\",\n\tRemoving = \"removing\",\n\tDead = \"dead\",\n\tEmpty = \"\",\n}\n\nexport enum HealthStatusEnum {\n\tEmpty = \"\",\n\tNone = \"none\",\n\tStarting = \"starting\",\n\tHealthy = \"healthy\",\n\tUnhealthy = \"unhealthy\",\n}\n\n/** HealthcheckResult stores information about a single run of a healthcheck probe */\nexport interface HealthcheckResult {\n\t/** Date and time at which this check started in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. */\n\tStart?: string;\n\t/** Date and time at which this check ended in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. */\n\tEnd?: string;\n\t/** ExitCode meanings:  - `0` healthy - `1` unhealthy - `2` reserved (considered unhealthy) - other values: error running probe */\n\tExitCode?: I64;\n\t/** Output from last check */\n\tOutput?: string;\n}\n\n/** Health stores information about the container's healthcheck results. */\nexport interface ContainerHealth {\n\t/** Status is one of `none`, `starting`, `healthy` or `unhealthy`  - \\\"none\\\"      Indicates there is no healthcheck - \\\"starting\\\"  Starting indicates that the container is not yet ready - \\\"healthy\\\"   Healthy indicates that the container is running correctly - \\\"unhealthy\\\" Unhealthy indicates that the container has a problem */\n\tStatus?: HealthStatusEnum;\n\t/** FailingStreak is the number of consecutive failures */\n\tFailingStreak?: I64;\n\t/** Log contains the last few results (oldest first) */\n\tLog?: HealthcheckResult[];\n}\n\n/** ContainerState stores container's running state. It's part of ContainerJSONBase and will be returned by the \\\"inspect\\\" command. */\nexport interface ContainerState {\n\t/** String representation of the container state. Can be one of \\\"created\\\", \\\"running\\\", \\\"paused\\\", \\\"restarting\\\", \\\"removing\\\", \\\"exited\\\", or \\\"dead\\\". */\n\tStatus?: ContainerStateStatusEnum;\n\t/** Whether this container is running.  Note that a running container can be _paused_. The `Running` and `Paused` booleans are not mutually exclusive:  When pausing a container (on Linux), the freezer cgroup is used to suspend all processes in the container. Freezing the process requires the process to be running. As a result, paused containers are both `Running` _and_ `Paused`.  Use the `Status` field instead to determine if a container's state is \\\"running\\\". */\n\tRunning?: boolean;\n\t/** Whether this container is paused. */\n\tPaused?: boolean;\n\t/** Whether this container is restarting. */\n\tRestarting?: boolean;\n\t/** Whether a process within this container has been killed because it ran out of memory since the container was last started. */\n\tOOMKilled?: boolean;\n\tDead?: boolean;\n\t/** The process ID of this container */\n\tPid?: I64;\n\t/** The last exit code of this container */\n\tExitCode?: I64;\n\tError?: string;\n\t/** The time when this container was last started. */\n\tStartedAt?: string;\n\t/** The time when this container last exited. */\n\tFinishedAt?: string;\n\tHealth?: ContainerHealth;\n}\n\nexport type Usize = number;\n\nexport interface ResourcesBlkioWeightDevice {\n\tPath?: string;\n\tWeight?: Usize;\n}\n\nexport interface ThrottleDevice {\n\t/** Device path */\n\tPath?: string;\n\t/** Rate */\n\tRate?: I64;\n}\n\n/** A device mapping between the host and container */\nexport interface DeviceMapping {\n\tPathOnHost?: string;\n\tPathInContainer?: string;\n\tCgroupPermissions?: string;\n}\n\n/** A request for devices to be sent to device drivers */\nexport interface DeviceRequest {\n\tDriver?: string;\n\tCount?: I64;\n\tDeviceIDs?: string[];\n\t/** A list of capabilities; an OR list of AND lists of capabilities. */\n\tCapabilities?: string[][];\n\t/** Driver-specific options, specified as a key/value pairs. These options are passed directly to the driver. */\n\tOptions?: Record<string, string>;\n}\n\nexport interface ResourcesUlimits {\n\t/** Name of ulimit */\n\tName?: string;\n\t/** Soft limit */\n\tSoft?: I64;\n\t/** Hard limit */\n\tHard?: I64;\n}\n\n/** The logging configuration for this container */\nexport interface HostConfigLogConfig {\n\tType?: string;\n\tConfig?: Record<string, string>;\n}\n\n/** PortBinding represents a binding between a host IP address and a host port. */\nexport interface PortBinding {\n\t/** Host IP address that the container's port is mapped to. */\n\tHostIp?: string;\n\t/** Host port number that the container's port is mapped to. */\n\tHostPort?: string;\n}\n\nexport enum RestartPolicyNameEnum {\n\tEmpty = \"\",\n\tNo = \"no\",\n\tAlways = \"always\",\n\tUnlessStopped = \"unless-stopped\",\n\tOnFailure = \"on-failure\",\n}\n\n/** The behavior to apply when the container exits. The default is not to restart.  An ever increasing delay (double the previous delay, starting at 100ms) is added before each restart to prevent flooding the server. */\nexport interface RestartPolicy {\n\t/** - Empty string means not to restart - `no` Do not automatically restart - `always` Always restart - `unless-stopped` Restart always except when the user has manually stopped the container - `on-failure` Restart only when the container exit code is non-zero */\n\tName?: RestartPolicyNameEnum;\n\t/** If `on-failure` is used, the number of times to retry before giving up. */\n\tMaximumRetryCount?: I64;\n}\n\nexport enum MountTypeEnum {\n\tEmpty = \"\",\n\tBind = \"bind\",\n\tVolume = \"volume\",\n\tImage = \"image\",\n\tTmpfs = \"tmpfs\",\n\tNpipe = \"npipe\",\n\tCluster = \"cluster\",\n}\n\nexport enum MountBindOptionsPropagationEnum {\n\tEmpty = \"\",\n\tPrivate = \"private\",\n\tRprivate = \"rprivate\",\n\tShared = \"shared\",\n\tRshared = \"rshared\",\n\tSlave = \"slave\",\n\tRslave = \"rslave\",\n}\n\n/** Optional configuration for the `bind` type. */\nexport interface MountBindOptions {\n\t/** A propagation mode with the value `[r]private`, `[r]shared`, or `[r]slave`. */\n\tPropagation?: MountBindOptionsPropagationEnum;\n\t/** Disable recursive bind mount. */\n\tNonRecursive?: boolean;\n\t/** Create mount point on host if missing */\n\tCreateMountpoint?: boolean;\n\t/** Make the mount non-recursively read-only, but still leave the mount recursive (unless NonRecursive is set to `true` in conjunction).  Addded in v1.44, before that version all read-only mounts were non-recursive by default. To match the previous behaviour this will default to `true` for clients on versions prior to v1.44. */\n\tReadOnlyNonRecursive?: boolean;\n\t/** Raise an error if the mount cannot be made recursively read-only. */\n\tReadOnlyForceRecursive?: boolean;\n}\n\n/** Map of driver specific options */\nexport interface MountVolumeOptionsDriverConfig {\n\t/** Name of the driver to use to create the volume. */\n\tName?: string;\n\t/** key/value map of driver specific options. */\n\tOptions?: Record<string, string>;\n}\n\n/** Optional configuration for the `volume` type. */\nexport interface MountVolumeOptions {\n\t/** Populate volume with data from the target. */\n\tNoCopy?: boolean;\n\t/** User-defined key/value metadata. */\n\tLabels?: Record<string, string>;\n\tDriverConfig?: MountVolumeOptionsDriverConfig;\n\t/** Source path inside the volume. Must be relative without any back traversals. */\n\tSubpath?: string;\n}\n\n/** Optional configuration for the `tmpfs` type. */\nexport interface MountTmpfsOptions {\n\t/** The size for the tmpfs mount in bytes. */\n\tSizeBytes?: I64;\n\t/** The permission mode for the tmpfs mount in an integer. */\n\tMode?: I64;\n}\n\nexport interface ContainerMount {\n\t/** Container path. */\n\tTarget?: string;\n\t/** Mount source (e.g. a volume name, a host path). */\n\tSource?: string;\n\t/** The mount type. Available types:  - `bind` Mounts a file or directory from the host into the container. Must exist prior to creating the container. - `volume` Creates a volume with the given name and options (or uses a pre-existing volume with the same name and options). These are **not** removed when the container is removed. - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container. - `cluster` a Swarm cluster volume */\n\tType?: MountTypeEnum;\n\t/** Whether the mount should be read-only. */\n\tReadOnly?: boolean;\n\t/** The consistency requirement for the mount: `default`, `consistent`, `cached`, or `delegated`. */\n\tConsistency?: string;\n\tBindOptions?: MountBindOptions;\n\tVolumeOptions?: MountVolumeOptions;\n\tTmpfsOptions?: MountTmpfsOptions;\n}\n\nexport enum HostConfigCgroupnsModeEnum {\n\tEmpty = \"\",\n\tPrivate = \"private\",\n\tHost = \"host\",\n}\n\nexport enum HostConfigIsolationEnum {\n\tEmpty = \"\",\n\tDefault = \"default\",\n\tProcess = \"process\",\n\tHyperv = \"hyperv\",\n}\n\n/** Container configuration that depends on the host we are running on */\nexport interface HostConfig {\n\t/** An integer value representing this container's relative CPU weight versus other containers. */\n\tCpuShares?: I64;\n\t/** Memory limit in bytes. */\n\tMemory?: I64;\n\t/** Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist. */\n\tCgroupParent?: string;\n\t/** Block IO weight (relative weight). */\n\tBlkioWeight?: number;\n\t/** Block IO weight (relative device weight) in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Weight\\\": weight}] ``` */\n\tBlkioWeightDevice?: ResourcesBlkioWeightDevice[];\n\t/** Limit read rate (bytes per second) from a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ``` */\n\tBlkioDeviceReadBps?: ThrottleDevice[];\n\t/** Limit write rate (bytes per second) to a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ``` */\n\tBlkioDeviceWriteBps?: ThrottleDevice[];\n\t/** Limit read rate (IO per second) from a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ``` */\n\tBlkioDeviceReadIOps?: ThrottleDevice[];\n\t/** Limit write rate (IO per second) to a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ``` */\n\tBlkioDeviceWriteIOps?: ThrottleDevice[];\n\t/** The length of a CPU period in microseconds. */\n\tCpuPeriod?: I64;\n\t/** Microseconds of CPU time that the container can get in a CPU period. */\n\tCpuQuota?: I64;\n\t/** The length of a CPU real-time period in microseconds. Set to 0 to allocate no time allocated to real-time tasks. */\n\tCpuRealtimePeriod?: I64;\n\t/** The length of a CPU real-time runtime in microseconds. Set to 0 to allocate no time allocated to real-time tasks. */\n\tCpuRealtimeRuntime?: I64;\n\t/** CPUs in which to allow execution (e.g., `0-3`, `0,1`). */\n\tCpusetCpus?: string;\n\t/** Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. */\n\tCpusetMems?: string;\n\t/** A list of devices to add to the container. */\n\tDevices?: DeviceMapping[];\n\t/** a list of cgroup rules to apply to the container */\n\tDeviceCgroupRules?: string[];\n\t/** A list of requests for devices to be sent to device drivers. */\n\tDeviceRequests?: DeviceRequest[];\n\t/** Hard limit for kernel TCP buffer memory (in bytes). Depending on the OCI runtime in use, this option may be ignored. It is no longer supported by the default (runc) runtime.  This field is omitted when empty. */\n\tKernelMemoryTCP?: I64;\n\t/** Memory soft limit in bytes. */\n\tMemoryReservation?: I64;\n\t/** Total memory limit (memory + swap). Set as `-1` to enable unlimited swap. */\n\tMemorySwap?: I64;\n\t/** Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. */\n\tMemorySwappiness?: I64;\n\t/** CPU quota in units of 10<sup>-9</sup> CPUs. */\n\tNanoCpus?: I64;\n\t/** Disable OOM Killer for the container. */\n\tOomKillDisable?: boolean;\n\t/** Run an init inside the container that forwards signals and reaps processes. This field is omitted if empty, and the default (as configured on the daemon) is used. */\n\tInit?: boolean;\n\t/** Tune a container's PIDs limit. Set `0` or `-1` for unlimited, or `null` to not change. */\n\tPidsLimit?: I64;\n\t/** A list of resource limits to set in the container. For example:  ``` {\\\"Name\\\": \\\"nofile\\\", \\\"Soft\\\": 1024, \\\"Hard\\\": 2048} ``` */\n\tUlimits?: ResourcesUlimits[];\n\t/** The number of usable CPUs (Windows only).  On Windows Server containers, the processor resource controls are mutually exclusive. The order of precedence is `CPUCount` first, then `CPUShares`, and `CPUPercent` last. */\n\tCpuCount?: I64;\n\t/** The usable percentage of the available CPUs (Windows only).  On Windows Server containers, the processor resource controls are mutually exclusive. The order of precedence is `CPUCount` first, then `CPUShares`, and `CPUPercent` last. */\n\tCpuPercent?: I64;\n\t/** Maximum IOps for the container system drive (Windows only) */\n\tIOMaximumIOps?: I64;\n\t/** Maximum IO in bytes per second for the container system drive (Windows only). */\n\tIOMaximumBandwidth?: I64;\n\t/** A list of volume bindings for this container. Each volume binding is a string in one of these forms:  - `host-src:container-dest[:options]` to bind-mount a host path   into the container. Both `host-src`, and `container-dest` must   be an _absolute_ path. - `volume-name:container-dest[:options]` to bind-mount a volume   managed by a volume driver into the container. `container-dest`   must be an _absolute_ path.  `options` is an optional, comma-delimited list of:  - `nocopy` disables automatic copying of data from the container   path to the volume. The `nocopy` flag only applies to named volumes. - `[ro|rw]` mounts a volume read-only or read-write, respectively.   If omitted or set to `rw`, volumes are mounted read-write. - `[z|Z]` applies SELinux labels to allow or deny multiple containers   to read and write to the same volume.     - `z`: a _shared_ content label is applied to the content. This       label indicates that multiple containers can share the volume       content, for both reading and writing.     - `Z`: a _private unshared_ label is applied to the content.       This label indicates that only the current container can use       a private volume. Labeling systems such as SELinux require       proper labels to be placed on volume content that is mounted       into a container. Without a label, the security system can       prevent a container's processes from using the content. By       default, the labels set by the host operating system are not       modified. - `[[r]shared|[r]slave|[r]private]` specifies mount   [propagation behavior](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt).   This only applies to bind-mounted volumes, not internal volumes   or named volumes. Mount propagation requires the source mount   point (the location where the source directory is mounted in the   host operating system) to have the correct propagation properties.   For shared volumes, the source mount point must be set to `shared`.   For slave volumes, the mount must be set to either `shared` or   `slave`. */\n\tBinds?: string[];\n\t/** Path to a file where the container ID is written */\n\tContainerIDFile?: string;\n\tLogConfig?: HostConfigLogConfig;\n\t/** Network mode to use for this container. Supported standard values are: `bridge`, `host`, `none`, and `container:<name|id>`. Any other value is taken as a custom network's name to which this container should connect to. */\n\tNetworkMode?: string;\n\tPortBindings?: Record<string, PortBinding[]>;\n\tRestartPolicy?: RestartPolicy;\n\t/** Automatically remove the container when the container's process exits. This has no effect if `RestartPolicy` is set. */\n\tAutoRemove?: boolean;\n\t/** Driver that this container uses to mount volumes. */\n\tVolumeDriver?: string;\n\t/** A list of volumes to inherit from another container, specified in the form `<container name>[:<ro|rw>]`. */\n\tVolumesFrom?: string[];\n\t/** Specification for mounts to be added to the container. */\n\tMounts?: ContainerMount[];\n\t/** Initial console size, as an `[height, width]` array. */\n\tConsoleSize?: number[];\n\t/** Arbitrary non-identifying metadata attached to container and provided to the runtime when the container is started. */\n\tAnnotations?: Record<string, string>;\n\t/** A list of kernel capabilities to add to the container. Conflicts with option 'Capabilities'. */\n\tCapAdd?: string[];\n\t/** A list of kernel capabilities to drop from the container. Conflicts with option 'Capabilities'. */\n\tCapDrop?: string[];\n\t/** cgroup namespace mode for the container. Possible values are:  - `\\\"private\\\"`: the container runs in its own private cgroup namespace - `\\\"host\\\"`: use the host system's cgroup namespace  If not specified, the daemon default is used, which can either be `\\\"private\\\"` or `\\\"host\\\"`, depending on daemon version, kernel support and configuration. */\n\tCgroupnsMode?: HostConfigCgroupnsModeEnum;\n\t/** A list of DNS servers for the container to use. */\n\tDns?: string[];\n\t/** A list of DNS options. */\n\tDnsOptions?: string[];\n\t/** A list of DNS search domains. */\n\tDnsSearch?: string[];\n\t/** A list of hostnames/IP mappings to add to the container's `/etc/hosts` file. Specified in the form `[\\\"hostname:IP\\\"]`. */\n\tExtraHosts?: string[];\n\t/** A list of additional groups that the container process will run as. */\n\tGroupAdd?: string[];\n\t/** IPC sharing mode for the container. Possible values are:  - `\\\"none\\\"`: own private IPC namespace, with /dev/shm not mounted - `\\\"private\\\"`: own private IPC namespace - `\\\"shareable\\\"`: own private IPC namespace, with a possibility to share it with other containers - `\\\"container:<name|id>\\\"`: join another (shareable) container's IPC namespace - `\\\"host\\\"`: use the host system's IPC namespace  If not specified, daemon default is used, which can either be `\\\"private\\\"` or `\\\"shareable\\\"`, depending on daemon version and configuration. */\n\tIpcMode?: string;\n\t/** Cgroup to use for the container. */\n\tCgroup?: string;\n\t/** A list of links for the container in the form `container_name:alias`. */\n\tLinks?: string[];\n\t/** An integer value containing the score given to the container in order to tune OOM killer preferences. */\n\tOomScoreAdj?: I64;\n\t/** Set the PID (Process) Namespace mode for the container. It can be either:  - `\\\"container:<name|id>\\\"`: joins another container's PID namespace - `\\\"host\\\"`: use the host's PID namespace inside the container */\n\tPidMode?: string;\n\t/** Gives the container full access to the host. */\n\tPrivileged?: boolean;\n\t/** Allocates an ephemeral host port for all of a container's exposed ports.  Ports are de-allocated when the container stops and allocated when the container starts. The allocated port might be changed when restarting the container.  The port is selected from the ephemeral port range that depends on the kernel. For example, on Linux the range is defined by `/proc/sys/net/ipv4/ip_local_port_range`. */\n\tPublishAllPorts?: boolean;\n\t/** Mount the container's root filesystem as read only. */\n\tReadonlyRootfs?: boolean;\n\t/** A list of string values to customize labels for MLS systems, such as SELinux. */\n\tSecurityOpt?: string[];\n\t/** Storage driver options for this container, in the form `{\\\"size\\\": \\\"120G\\\"}`. */\n\tStorageOpt?: Record<string, string>;\n\t/** A map of container directories which should be replaced by tmpfs mounts, and their corresponding mount options. For example:  ``` { \\\"/run\\\": \\\"rw,noexec,nosuid,size=65536k\\\" } ``` */\n\tTmpfs?: Record<string, string>;\n\t/** UTS namespace to use for the container. */\n\tUTSMode?: string;\n\t/** Sets the usernamespace mode for the container when usernamespace remapping option is enabled. */\n\tUsernsMode?: string;\n\t/** Size of `/dev/shm` in bytes. If omitted, the system uses 64MB. */\n\tShmSize?: I64;\n\t/** A list of kernel parameters (sysctls) to set in the container. For example:  ``` {\\\"net.ipv4.ip_forward\\\": \\\"1\\\"} ``` */\n\tSysctls?: Record<string, string>;\n\t/** Runtime to use with this container. */\n\tRuntime?: string;\n\t/** Isolation technology of the container. (Windows only) */\n\tIsolation?: HostConfigIsolationEnum;\n\t/** The list of paths to be masked inside the container (this overrides the default set of paths). */\n\tMaskedPaths?: string[];\n\t/** The list of paths to be set as read-only inside the container (this overrides the default set of paths). */\n\tReadonlyPaths?: string[];\n}\n\n/** Information about the storage driver used to store the container's and image's filesystem. */\nexport interface GraphDriverData {\n\t/** Name of the storage driver. */\n\tName?: string;\n\t/** Low-level storage metadata, provided as key/value pairs.  This information is driver-specific, and depends on the storage-driver in use, and should be used for informational purposes only. */\n\tData?: Record<string, string>;\n}\n\n/** MountPoint represents a mount point configuration inside the container. This is used for reporting the mountpoints in use by a container. */\nexport interface MountPoint {\n\t/** The mount type:  - `bind` a mount of a file or directory from the host into the container. - `volume` a docker volume with the given `Name`. - `tmpfs` a `tmpfs`. - `npipe` a named pipe from the host into the container. - `cluster` a Swarm cluster volume */\n\tType?: MountTypeEnum;\n\t/** Name is the name reference to the underlying data defined by `Source` e.g., the volume name. */\n\tName?: string;\n\t/** Source location of the mount.  For volumes, this contains the storage location of the volume (within `/var/lib/docker/volumes/`). For bind-mounts, and `npipe`, this contains the source (host) part of the bind-mount. For `tmpfs` mount points, this field is empty. */\n\tSource?: string;\n\t/** Destination is the path relative to the container root (`/`) where the `Source` is mounted inside the container. */\n\tDestination?: string;\n\t/** Driver is the volume driver used to create the volume (if it is a volume). */\n\tDriver?: string;\n\t/** Mode is a comma separated list of options supplied by the user when creating the bind/volume mount.  The default is platform-specific (`\\\"z\\\"` on Linux, empty on Windows). */\n\tMode?: string;\n\t/** Whether the mount is mounted writable (read-write). */\n\tRW?: boolean;\n\t/** Propagation describes how mounts are propagated from the host into the mount point, and vice-versa. Refer to the [Linux kernel documentation](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt) for details. This field is not used on Windows. */\n\tPropagation?: string;\n}\n\n/** A test to perform to check that the container is healthy. */\nexport interface HealthConfig {\n\t/** The test to perform. Possible values are:  - `[]` inherit healthcheck from image or parent image - `[\\\"NONE\\\"]` disable healthcheck - `[\\\"CMD\\\", args...]` exec arguments directly - `[\\\"CMD-SHELL\\\", command]` run command with system's default shell */\n\tTest?: string[];\n\t/** The time to wait between checks in nanoseconds. It should be 0 or at least 1000000 (1 ms). 0 means inherit. */\n\tInterval?: I64;\n\t/** The time to wait before considering the check to have hung. It should be 0 or at least 1000000 (1 ms). 0 means inherit. */\n\tTimeout?: I64;\n\t/** The number of consecutive failures needed to consider a container as unhealthy. 0 means inherit. */\n\tRetries?: I64;\n\t/** Start period for the container to initialize before starting health-retries countdown in nanoseconds. It should be 0 or at least 1000000 (1 ms). 0 means inherit. */\n\tStartPeriod?: I64;\n\t/** The time to wait between checks in nanoseconds during the start period. It should be 0 or at least 1000000 (1 ms). 0 means inherit. */\n\tStartInterval?: I64;\n}\n\n/** Configuration for a container that is portable between hosts.  When used as `ContainerConfig` field in an image, `ContainerConfig` is an optional field containing the configuration of the container that was last committed when creating the image.  Previous versions of Docker builder used this field to store build cache, and it is not in active use anymore. */\nexport interface ContainerConfig {\n\t/** The hostname to use for the container, as a valid RFC 1123 hostname. */\n\tHostname?: string;\n\t/** The domain name to use for the container. */\n\tDomainname?: string;\n\t/** The user that commands are run as inside the container. */\n\tUser?: string;\n\t/** Whether to attach to `stdin`. */\n\tAttachStdin?: boolean;\n\t/** Whether to attach to `stdout`. */\n\tAttachStdout?: boolean;\n\t/** Whether to attach to `stderr`. */\n\tAttachStderr?: boolean;\n\t/** An object mapping ports to an empty object in the form:  `{\\\"<port>/<tcp|udp|sctp>\\\": {}}` */\n\tExposedPorts?: Record<string, Record<string, undefined>>;\n\t/** Attach standard streams to a TTY, including `stdin` if it is not closed. */\n\tTty?: boolean;\n\t/** Open `stdin` */\n\tOpenStdin?: boolean;\n\t/** Close `stdin` after one attached client disconnects */\n\tStdinOnce?: boolean;\n\t/** A list of environment variables to set inside the container in the form `[\\\"VAR=value\\\", ...]`. A variable without `=` is removed from the environment, rather than to have an empty value. */\n\tEnv?: string[];\n\t/** Command to run specified as a string or an array of strings. */\n\tCmd?: string[];\n\tHealthcheck?: HealthConfig;\n\t/** Command is already escaped (Windows only) */\n\tArgsEscaped?: boolean;\n\t/** The name (or reference) of the image to use when creating the container, or which was used when the container was created. */\n\tImage?: string;\n\t/** An object mapping mount point paths inside the container to empty objects. */\n\tVolumes?: Record<string, Record<string, undefined>>;\n\t/** The working directory for commands to run in. */\n\tWorkingDir?: string;\n\t/** The entry point for the container as a string or an array of strings.  If the array consists of exactly one empty string (`[\\\"\\\"]`) then the entry point is reset to system default (i.e., the entry point used by docker when there is no `ENTRYPOINT` instruction in the `Dockerfile`). */\n\tEntrypoint?: string[];\n\t/** Disable networking for the container. */\n\tNetworkDisabled?: boolean;\n\t/** MAC address of the container.  Deprecated: this field is deprecated in API v1.44 and up. Use EndpointSettings.MacAddress instead. */\n\tMacAddress?: string;\n\t/** `ONBUILD` metadata that were defined in the image's `Dockerfile`. */\n\tOnBuild?: string[];\n\t/** User-defined key/value metadata. */\n\tLabels?: Record<string, string>;\n\t/** Signal to stop a container as a string or unsigned integer. */\n\tStopSignal?: string;\n\t/** Timeout to stop a container in seconds. */\n\tStopTimeout?: I64;\n\t/** Shell for when `RUN`, `CMD`, and `ENTRYPOINT` uses a shell. */\n\tShell?: string[];\n}\n\n/** EndpointIPAMConfig represents an endpoint's IPAM configuration. */\nexport interface EndpointIpamConfig {\n\tIPv4Address?: string;\n\tIPv6Address?: string;\n\tLinkLocalIPs?: string[];\n}\n\n/** Configuration for a network endpoint. */\nexport interface EndpointSettings {\n\tIPAMConfig?: EndpointIpamConfig;\n\tLinks?: string[];\n\t/** MAC address for the endpoint on this network. The network driver might ignore this parameter. */\n\tMacAddress?: string;\n\tAliases?: string[];\n\t/** Unique ID of the network. */\n\tNetworkID?: string;\n\t/** Unique ID for the service endpoint in a Sandbox. */\n\tEndpointID?: string;\n\t/** Gateway address for this network. */\n\tGateway?: string;\n\t/** IPv4 address. */\n\tIPAddress?: string;\n\t/** Mask length of the IPv4 address. */\n\tIPPrefixLen?: I64;\n\t/** IPv6 gateway address. */\n\tIPv6Gateway?: string;\n\t/** Global IPv6 address. */\n\tGlobalIPv6Address?: string;\n\t/** Mask length of the global IPv6 address. */\n\tGlobalIPv6PrefixLen?: I64;\n\t/** DriverOpts is a mapping of driver options and values. These options are passed directly to the driver and are driver specific. */\n\tDriverOpts?: Record<string, string>;\n\t/** List of all DNS names an endpoint has on a specific network. This list is based on the container name, network aliases, container short ID, and hostname.  These DNS names are non-fully qualified but can contain several dots. You can get fully qualified DNS names by appending `.<network-name>`. For instance, if container name is `my.ctr` and the network is named `testnet`, `DNSNames` will contain `my.ctr` and the FQDN will be `my.ctr.testnet`. */\n\tDNSNames?: string[];\n}\n\n/** NetworkSettings exposes the network settings in the API */\nexport interface NetworkSettings {\n\t/** Name of the default bridge interface when dockerd's --bridge flag is set. */\n\tBridge?: string;\n\t/** SandboxID uniquely represents a container's network stack. */\n\tSandboxID?: string;\n\tPorts?: Record<string, PortBinding[]>;\n\t/** SandboxKey is the full path of the netns handle */\n\tSandboxKey?: string;\n\t/** Information about all networks that the container is connected to. */\n\tNetworks?: Record<string, EndpointSettings>;\n}\n\nexport interface Container {\n\t/** The ID of the container */\n\tId?: string;\n\t/** The time the container was created */\n\tCreated?: string;\n\t/** The path to the command being run */\n\tPath?: string;\n\t/** The arguments to the command being run */\n\tArgs?: string[];\n\tState?: ContainerState;\n\t/** The container's image ID */\n\tImage?: string;\n\tResolvConfPath?: string;\n\tHostnamePath?: string;\n\tHostsPath?: string;\n\tLogPath?: string;\n\tName?: string;\n\tRestartCount?: I64;\n\tDriver?: string;\n\tPlatform?: string;\n\tMountLabel?: string;\n\tProcessLabel?: string;\n\tAppArmorProfile?: string;\n\t/** IDs of exec instances that are running in the container. */\n\tExecIDs?: string[];\n\tHostConfig?: HostConfig;\n\tGraphDriver?: GraphDriverData;\n\t/** The size of files that have been created or changed by this container. */\n\tSizeRw?: I64;\n\t/** The total size of all the files in this container. */\n\tSizeRootFs?: I64;\n\tMounts?: MountPoint[];\n\tConfig?: ContainerConfig;\n\tNetworkSettings?: NetworkSettings;\n}\n\nexport type InspectDeploymentContainerResponse = Container;\n\nexport type InspectDockerContainerResponse = Container;\n\n/** Information about the image's RootFS, including the layer IDs. */\nexport interface ImageInspectRootFs {\n\tType?: string;\n\tLayers?: string[];\n}\n\n/** Additional metadata of the image in the local cache. This information is local to the daemon, and not part of the image itself. */\nexport interface ImageInspectMetadata {\n\t/** Date and time at which the image was last tagged in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.  This information is only available if the image was tagged locally, and omitted otherwise. */\n\tLastTagTime?: string;\n}\n\n/** Information about an image in the local image cache. */\nexport interface Image {\n\t/** ID is the content-addressable ID of an image.  This identifier is a content-addressable digest calculated from the image's configuration (which includes the digests of layers used by the image).  Note that this digest differs from the `RepoDigests` below, which holds digests of image manifests that reference the image. */\n\tId?: string;\n\t/** List of image names/tags in the local image cache that reference this image.  Multiple image tags can refer to the same image, and this list may be empty if no tags reference the image, in which case the image is \\\"untagged\\\", in which case it can still be referenced by its ID. */\n\tRepoTags?: string[];\n\t/** List of content-addressable digests of locally available image manifests that the image is referenced from. Multiple manifests can refer to the same image.  These digests are usually only available if the image was either pulled from a registry, or if the image was pushed to a registry, which is when the manifest is generated and its digest calculated. */\n\tRepoDigests?: string[];\n\t/** ID of the parent image.  Depending on how the image was created, this field may be empty and is only set for images that were built/created locally. This field is empty if the image was pulled from an image registry. */\n\tParent?: string;\n\t/** Optional message that was set when committing or importing the image. */\n\tComment?: string;\n\t/** Date and time at which the image was created, formatted in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.  This information is only available if present in the image, and omitted otherwise. */\n\tCreated?: string;\n\t/** The version of Docker that was used to build the image.  Depending on how the image was created, this field may be empty. */\n\tDockerVersion?: string;\n\t/** Name of the author that was specified when committing the image, or as specified through MAINTAINER (deprecated) in the Dockerfile. */\n\tAuthor?: string;\n\t/** Configuration for a container that is portable between hosts. */\n\tConfig?: ContainerConfig;\n\t/** Hardware CPU architecture that the image runs on. */\n\tArchitecture?: string;\n\t/** CPU architecture variant (presently ARM-only). */\n\tVariant?: string;\n\t/** Operating System the image is built to run on. */\n\tOs?: string;\n\t/** Operating System version the image is built to run on (especially for Windows). */\n\tOsVersion?: string;\n\t/** Total size of the image including all layers it is composed of. */\n\tSize?: I64;\n\tGraphDriver?: GraphDriverData;\n\tRootFS?: ImageInspectRootFs;\n\tMetadata?: ImageInspectMetadata;\n}\n\nexport type InspectDockerImageResponse = Image;\n\nexport interface IpamConfig {\n\tSubnet?: string;\n\tIPRange?: string;\n\tGateway?: string;\n\tAuxiliaryAddresses: Record<string, string>;\n}\n\nexport interface Ipam {\n\t/** Name of the IPAM driver to use. */\n\tDriver?: string;\n\t/** List of IPAM configuration options, specified as a map:  ``` {\\\"Subnet\\\": <CIDR>, \\\"IPRange\\\": <CIDR>, \\\"Gateway\\\": <IP address>, \\\"AuxAddress\\\": <device_name:IP address>} ``` */\n\tConfig: IpamConfig[];\n\t/** Driver-specific options, specified as a map. */\n\tOptions: Record<string, string>;\n}\n\nexport interface NetworkContainer {\n\t/** This is the key on the incoming map of NetworkContainer */\n\tContainerID?: string;\n\tName?: string;\n\tEndpointID?: string;\n\tMacAddress?: string;\n\tIPv4Address?: string;\n\tIPv6Address?: string;\n}\n\nexport interface Network {\n\tName?: string;\n\tId?: string;\n\tCreated?: string;\n\tScope?: string;\n\tDriver?: string;\n\tEnableIPv6?: boolean;\n\tIPAM?: Ipam;\n\tInternal?: boolean;\n\tAttachable?: boolean;\n\tIngress?: boolean;\n\t/** This field is turned from map into array for easier usability. */\n\tContainers: NetworkContainer[];\n\tOptions?: Record<string, string>;\n\tLabels?: Record<string, string>;\n}\n\nexport type InspectDockerNetworkResponse = Network;\n\nexport enum VolumeScopeEnum {\n\tEmpty = \"\",\n\tLocal = \"local\",\n\tGlobal = \"global\",\n}\n\nexport type U64 = number;\n\n/** The version number of the object such as node, service, etc. This is needed to avoid conflicting writes. The client must send the version number along with the modified specification when updating these objects.  This approach ensures safe concurrency and determinism in that the change on the object may not be applied if the version number has changed from the last read. In other words, if two update requests specify the same base version, only one of the requests can succeed. As a result, two separate update requests that happen at the same time will not unintentionally overwrite each other. */\nexport interface ObjectVersion {\n\tIndex?: U64;\n}\n\nexport enum ClusterVolumeSpecAccessModeScopeEnum {\n\tEmpty = \"\",\n\tSingle = \"single\",\n\tMulti = \"multi\",\n}\n\nexport enum ClusterVolumeSpecAccessModeSharingEnum {\n\tEmpty = \"\",\n\tNone = \"none\",\n\tReadonly = \"readonly\",\n\tOnewriter = \"onewriter\",\n\tAll = \"all\",\n}\n\n/** One cluster volume secret entry. Defines a key-value pair that is passed to the plugin. */\nexport interface ClusterVolumeSpecAccessModeSecrets {\n\t/** Key is the name of the key of the key-value pair passed to the plugin. */\n\tKey?: string;\n\t/** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */\n\tSecret?: string;\n}\n\nexport type Topology = Record<string, PortBinding[]>;\n\n/** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */\nexport interface ClusterVolumeSpecAccessModeAccessibilityRequirements {\n\t/** A list of required topologies, at least one of which the volume must be accessible from. */\n\tRequisite?: Topology[];\n\t/** A list of topologies that the volume should attempt to be provisioned in. */\n\tPreferred?: Topology[];\n}\n\n/** The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity. */\nexport interface ClusterVolumeSpecAccessModeCapacityRange {\n\t/** The volume must be at least this big. The value of 0 indicates an unspecified minimum */\n\tRequiredBytes?: I64;\n\t/** The volume must not be bigger than this. The value of 0 indicates an unspecified maximum. */\n\tLimitBytes?: I64;\n}\n\nexport enum ClusterVolumeSpecAccessModeAvailabilityEnum {\n\tEmpty = \"\",\n\tActive = \"active\",\n\tPause = \"pause\",\n\tDrain = \"drain\",\n}\n\n/** Defines how the volume is used by tasks. */\nexport interface ClusterVolumeSpecAccessMode {\n\t/** The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time. */\n\tScope?: ClusterVolumeSpecAccessModeScopeEnum;\n\t/** The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers. */\n\tSharing?: ClusterVolumeSpecAccessModeSharingEnum;\n\t/** Swarm Secrets that are passed to the CSI storage plugin when operating on this volume. */\n\tSecrets?: ClusterVolumeSpecAccessModeSecrets[];\n\tAccessibilityRequirements?: ClusterVolumeSpecAccessModeAccessibilityRequirements;\n\tCapacityRange?: ClusterVolumeSpecAccessModeCapacityRange;\n\t/** The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. */\n\tAvailability?: ClusterVolumeSpecAccessModeAvailabilityEnum;\n}\n\n/** Cluster-specific options used to create the volume. */\nexport interface ClusterVolumeSpec {\n\t/** Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services.  Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group. */\n\tGroup?: string;\n\tAccessMode?: ClusterVolumeSpecAccessMode;\n}\n\n/** Information about the global status of the volume. */\nexport interface ClusterVolumeInfo {\n\t/** The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown. */\n\tCapacityBytes?: I64;\n\t/** A map of strings to strings returned from the storage plugin when the volume is created. */\n\tVolumeContext?: Record<string, string>;\n\t/** The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet. */\n\tVolumeID?: string;\n\t/** The topology this volume is actually accessible from. */\n\tAccessibleTopology?: Topology[];\n}\n\nexport enum ClusterVolumePublishStatusStateEnum {\n\tEmpty = \"\",\n\tPendingPublish = \"pending-publish\",\n\tPublished = \"published\",\n\tPendingNodeUnpublish = \"pending-node-unpublish\",\n\tPendingControllerUnpublish = \"pending-controller-unpublish\",\n}\n\nexport interface ClusterVolumePublishStatus {\n\t/** The ID of the Swarm node the volume is published on. */\n\tNodeID?: string;\n\t/** The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. */\n\tState?: ClusterVolumePublishStatusStateEnum;\n\t/** A map of strings to strings returned by the CSI controller plugin when a volume is published. */\n\tPublishContext?: Record<string, string>;\n}\n\n/** Options and information specific to, and only present on, Swarm CSI cluster volumes. */\nexport interface ClusterVolume {\n\t/** The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name. */\n\tID?: string;\n\tVersion?: ObjectVersion;\n\tCreatedAt?: string;\n\tUpdatedAt?: string;\n\tSpec?: ClusterVolumeSpec;\n\tInfo?: ClusterVolumeInfo;\n\t/** The status of the volume as it pertains to its publishing and use on specific nodes */\n\tPublishStatus?: ClusterVolumePublishStatus[];\n}\n\n/** Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints. */\nexport interface VolumeUsageData {\n\t/** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\\\"local\\\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\\\"not available\\\") */\n\tSize: I64;\n\t/** The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available. */\n\tRefCount: I64;\n}\n\nexport interface Volume {\n\t/** Name of the volume. */\n\tName: string;\n\t/** Name of the volume driver used by the volume. */\n\tDriver: string;\n\t/** Mount path of the volume on the host. */\n\tMountpoint: string;\n\t/** Date/Time the volume was created. */\n\tCreatedAt?: string;\n\t/** Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\\\"key\\\":\\\"value\\\",\\\"key2\\\":\\\"value2\\\"}`.  The `Status` field is optional, and is omitted if the volume driver does not support this feature. */\n\tStatus?: Record<string, Record<string, undefined>>;\n\t/** User-defined key/value metadata. */\n\tLabels?: Record<string, string>;\n\t/** The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. */\n\tScope?: VolumeScopeEnum;\n\tClusterVolume?: ClusterVolume;\n\t/** The driver specific options used when creating the volume. */\n\tOptions?: Record<string, string>;\n\tUsageData?: VolumeUsageData;\n}\n\nexport type InspectDockerVolumeResponse = Volume;\n\nexport type InspectStackContainerResponse = Container;\n\nexport type JsonObject = any;\n\nexport type JsonValue = any;\n\nexport type ListActionsResponse = ActionListItem[];\n\nexport type ListAlertersResponse = AlerterListItem[];\n\nexport enum PortTypeEnum {\n\tEMPTY = \"\",\n\tTCP = \"tcp\",\n\tUDP = \"udp\",\n\tSCTP = \"sctp\",\n}\n\n/** An open port on a container */\nexport interface Port {\n\t/** Host IP address that the container's port is mapped to */\n\tIP?: string;\n\t/** Port on the container */\n\tPrivatePort?: number;\n\t/** Port exposed on the host */\n\tPublicPort?: number;\n\tType?: PortTypeEnum;\n}\n\n/** Container summary returned by container list apis. */\nexport interface ContainerListItem {\n\t/** The Server which holds the container. */\n\tserver_id?: string;\n\t/** The first name in Names, not including the initial '/' */\n\tname: string;\n\t/** The ID of this container */\n\tid?: string;\n\t/** The name of the image used when creating this container */\n\timage?: string;\n\t/** The ID of the image that this container was created from */\n\timage_id?: string;\n\t/** When the container was created */\n\tcreated?: I64;\n\t/** The size of files that have been created or changed by this container */\n\tsize_rw?: I64;\n\t/** The total size of all the files in this container */\n\tsize_root_fs?: I64;\n\t/** The state of this container (e.g. `exited`) */\n\tstate: ContainerStateStatusEnum;\n\t/** Additional human-readable status of this container (e.g. `Exit 0`) */\n\tstatus?: string;\n\t/** The network mode */\n\tnetwork_mode?: string;\n\t/** The network names attached to container */\n\tnetworks?: string[];\n\t/** Port mappings for the container */\n\tports?: Port[];\n\t/** The volume names attached to container */\n\tvolumes?: string[];\n\t/** The container stats, if they can be retreived. */\n\tstats?: ContainerStats;\n\t/**\n\t * The labels attached to container.\n\t * It's too big to send with container list,\n\t * can get it using InspectContainer\n\t */\n\tlabels?: Record<string, string>;\n}\n\nexport type ListAllDockerContainersResponse = ContainerListItem[];\n\n/** An api key used to authenticate requests via request headers. */\nexport interface ApiKey {\n\t/** Unique key associated with secret */\n\tkey: string;\n\t/** Hash of the secret */\n\tsecret: string;\n\t/** User associated with the api key */\n\tuser_id: string;\n\t/** Name associated with the api key for management */\n\tname: string;\n\t/** Timestamp of key creation */\n\tcreated_at: I64;\n\t/** Expiry of key, or 0 if never expires */\n\texpires: I64;\n}\n\nexport type ListApiKeysForServiceUserResponse = ApiKey[];\n\nexport type ListApiKeysResponse = ApiKey[];\n\nexport interface BuildVersionResponseItem {\n\tversion: Version;\n\tts: I64;\n}\n\nexport type ListBuildVersionsResponse = BuildVersionResponseItem[];\n\nexport type ListBuildersResponse = BuilderListItem[];\n\nexport type ListBuildsResponse = BuildListItem[];\n\nexport type ListCommonBuildExtraArgsResponse = string[];\n\nexport type ListCommonDeploymentExtraArgsResponse = string[];\n\nexport type ListCommonStackBuildExtraArgsResponse = string[];\n\nexport type ListCommonStackExtraArgsResponse = string[];\n\nexport interface ComposeProject {\n\t/** The compose project name. */\n\tname: string;\n\t/** The status of the project, as returned by docker. */\n\tstatus?: string;\n\t/** The compose files included in the project. */\n\tcompose_files: string[];\n}\n\nexport type ListComposeProjectsResponse = ComposeProject[];\n\nexport type ListDeploymentsResponse = DeploymentListItem[];\n\nexport type ListDockerContainersResponse = ContainerListItem[];\n\n/** individual image layer information in response to ImageHistory operation */\nexport interface ImageHistoryResponseItem {\n\tId: string;\n\tCreated: I64;\n\tCreatedBy: string;\n\tTags?: string[];\n\tSize: I64;\n\tComment: string;\n}\n\nexport type ListDockerImageHistoryResponse = ImageHistoryResponseItem[];\n\nexport interface ImageListItem {\n\t/** The first tag in `repo_tags`, or Id if no tags. */\n\tname: string;\n\t/** ID is the content-addressable ID of an image.  This identifier is a content-addressable digest calculated from the image's configuration (which includes the digests of layers used by the image).  Note that this digest differs from the `RepoDigests` below, which holds digests of image manifests that reference the image. */\n\tid: string;\n\t/** ID of the parent image.  Depending on how the image was created, this field may be empty and is only set for images that were built/created locally. This field is empty if the image was pulled from an image registry. */\n\tparent_id: string;\n\t/** Date and time at which the image was created as a Unix timestamp (number of seconds sinds EPOCH). */\n\tcreated: I64;\n\t/** Total size of the image including all layers it is composed of. */\n\tsize: I64;\n\t/** Whether the image is in use by any container */\n\tin_use: boolean;\n}\n\nexport type ListDockerImagesResponse = ImageListItem[];\n\nexport interface NetworkListItem {\n\tname?: string;\n\tid?: string;\n\tcreated?: string;\n\tscope?: string;\n\tdriver?: string;\n\tenable_ipv6?: boolean;\n\tipam_driver?: string;\n\tipam_subnet?: string;\n\tipam_gateway?: string;\n\tinternal?: boolean;\n\tattachable?: boolean;\n\tingress?: boolean;\n\t/** Whether the network is attached to one or more containers */\n\tin_use: boolean;\n}\n\nexport type ListDockerNetworksResponse = NetworkListItem[];\n\nexport interface ProviderAccount {\n\t/** The account username. Required. */\n\tusername: string;\n\t/** The account access token. Required. */\n\ttoken?: string;\n}\n\nexport interface DockerRegistry {\n\t/** The docker provider domain. Default: `docker.io`. */\n\tdomain: string;\n\t/** The accounts on the registry. Required. */\n\taccounts: ProviderAccount[];\n\t/**\n\t * Available organizations on the registry provider.\n\t * Used to push an image under an organization's repo rather than an account's repo.\n\t */\n\torganizations?: string[];\n}\n\nexport type ListDockerRegistriesFromConfigResponse = DockerRegistry[];\n\nexport type ListDockerRegistryAccountsResponse = DockerRegistryAccount[];\n\nexport interface VolumeListItem {\n\t/** The name of the volume */\n\tname: string;\n\tdriver: string;\n\tmountpoint: string;\n\tcreated?: string;\n\tscope: VolumeScopeEnum;\n\t/** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\\\"local\\\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\\\"not available\\\") */\n\tsize?: I64;\n\t/** Whether the volume is currently attached to any container */\n\tin_use: boolean;\n}\n\nexport type ListDockerVolumesResponse = VolumeListItem[];\n\nexport type ListFullActionsResponse = Action[];\n\nexport type ListFullAlertersResponse = Alerter[];\n\nexport type ListFullBuildersResponse = Builder[];\n\nexport type ListFullBuildsResponse = Build[];\n\nexport type ListFullDeploymentsResponse = Deployment[];\n\nexport type ListFullProceduresResponse = Procedure[];\n\nexport type ListFullReposResponse = Repo[];\n\nexport type ListFullResourceSyncsResponse = ResourceSync[];\n\nexport type ListFullServersResponse = Server[];\n\nexport type ListFullStacksResponse = Stack[];\n\nexport type ListGitProviderAccountsResponse = GitProviderAccount[];\n\nexport interface GitProvider {\n\t/** The git provider domain. Default: `github.com`. */\n\tdomain: string;\n\t/** Whether to use https. Default: true. */\n\thttps: boolean;\n\t/** The accounts on the git provider. Required. */\n\taccounts: ProviderAccount[];\n}\n\nexport type ListGitProvidersFromConfigResponse = GitProvider[];\n\nexport type UserTarget = \n\t/** User Id */\n\t| { type: \"User\", id: string }\n\t/** UserGroup Id */\n\t| { type: \"UserGroup\", id: string };\n\n/** Representation of a User or UserGroups permission on a resource. */\nexport interface Permission {\n\t/** The id of the permission document */\n\t_id?: MongoId;\n\t/** The target User / UserGroup */\n\tuser_target: UserTarget;\n\t/** The target resource */\n\tresource_target: ResourceTarget;\n\t/** The permission level for the [user_target] on the [resource_target]. */\n\tlevel?: PermissionLevel;\n\t/** Any specific permissions for the [user_target] on the [resource_target]. */\n\tspecific?: Array<SpecificPermission>;\n}\n\nexport type ListPermissionsResponse = Permission[];\n\nexport enum ProcedureState {\n\t/** Currently running */\n\tRunning = \"Running\",\n\t/** Last run successful */\n\tOk = \"Ok\",\n\t/** Last run failed */\n\tFailed = \"Failed\",\n\t/** Other case (never run) */\n\tUnknown = \"Unknown\",\n}\n\nexport interface ProcedureListItemInfo {\n\t/** Number of stages procedure has. */\n\tstages: I64;\n\t/** Reflect whether last run successful / currently running. */\n\tstate: ProcedureState;\n\t/** Procedure last successful run timestamp in ms. */\n\tlast_run_at?: I64;\n\t/**\n\t * If the procedure has schedule enabled, this is the\n\t * next scheduled run time in unix ms.\n\t */\n\tnext_scheduled_run?: I64;\n\t/**\n\t * If there is an error parsing schedule expression,\n\t * it will be given here.\n\t */\n\tschedule_error?: string;\n}\n\nexport type ProcedureListItem = ResourceListItem<ProcedureListItemInfo>;\n\nexport type ListProceduresResponse = ProcedureListItem[];\n\nexport enum RepoState {\n\t/** Unknown case */\n\tUnknown = \"Unknown\",\n\t/** Last clone / pull successful (or never cloned) */\n\tOk = \"Ok\",\n\t/** Last clone / pull failed */\n\tFailed = \"Failed\",\n\t/** Currently cloning */\n\tCloning = \"Cloning\",\n\t/** Currently pulling */\n\tPulling = \"Pulling\",\n\t/** Currently building */\n\tBuilding = \"Building\",\n}\n\nexport interface RepoListItemInfo {\n\t/** The server that repo sits on. */\n\tserver_id: string;\n\t/** The builder that builds the repo. */\n\tbuilder_id: string;\n\t/** Repo last cloned / pulled timestamp in ms. */\n\tlast_pulled_at: I64;\n\t/** Repo last built timestamp in ms. */\n\tlast_built_at: I64;\n\t/** The git provider domain */\n\tgit_provider: string;\n\t/** The configured repo */\n\trepo: string;\n\t/** The configured branch */\n\tbranch: string;\n\t/** Full link to the repo. */\n\trepo_link: string;\n\t/** The repo state */\n\tstate: RepoState;\n\t/** If the repo is cloned, will be the cloned short commit hash. */\n\tcloned_hash?: string;\n\t/** If the repo is cloned, will be the cloned commit message. */\n\tcloned_message?: string;\n\t/** If the repo is built, will be the latest built short commit hash. */\n\tbuilt_hash?: string;\n\t/** Will be the latest remote short commit hash. */\n\tlatest_hash?: string;\n}\n\nexport type RepoListItem = ResourceListItem<RepoListItemInfo>;\n\nexport type ListReposResponse = RepoListItem[];\n\nexport enum ResourceSyncState {\n\t/** Currently syncing */\n\tSyncing = \"Syncing\",\n\t/** Updates pending */\n\tPending = \"Pending\",\n\t/** Last sync successful (or never synced). No Changes pending */\n\tOk = \"Ok\",\n\t/** Last sync failed */\n\tFailed = \"Failed\",\n\t/** Other case */\n\tUnknown = \"Unknown\",\n}\n\nexport interface ResourceSyncListItemInfo {\n\t/** Unix timestamp of last sync, or 0 */\n\tlast_sync_ts: I64;\n\t/** Whether sync is `files_on_host` mode. */\n\tfiles_on_host: boolean;\n\t/** Whether sync has file contents defined. */\n\tfile_contents: boolean;\n\t/** Whether sync has `managed` mode enabled. */\n\tmanaged: boolean;\n\t/** Resource paths to the files. */\n\tresource_path: string[];\n\t/** Linked repo, if one is attached. */\n\tlinked_repo: string;\n\t/** The git provider domain. */\n\tgit_provider: string;\n\t/** The Github repo used as the source of the sync resources */\n\trepo: string;\n\t/** The branch of the repo */\n\tbranch: string;\n\t/** Full link to the repo. */\n\trepo_link: string;\n\t/** Short commit hash of last sync, or empty string */\n\tlast_sync_hash?: string;\n\t/** Commit message of last sync, or empty string */\n\tlast_sync_message?: string;\n\t/** State of the sync. Reflects whether most recent sync successful. */\n\tstate: ResourceSyncState;\n}\n\nexport type ResourceSyncListItem = ResourceListItem<ResourceSyncListItemInfo>;\n\nexport type ListResourceSyncsResponse = ResourceSyncListItem[];\n\n/** A scheduled Action / Procedure run. */\nexport interface Schedule {\n\t/** Procedure or Alerter */\n\ttarget: ResourceTarget;\n\t/** Readable name of the target resource */\n\tname: string;\n\t/** The format of the schedule expression */\n\tschedule_format: ScheduleFormat;\n\t/** The schedule for the run */\n\tschedule: string;\n\t/** Whether the scheduled run is enabled */\n\tenabled: boolean;\n\t/** Custom schedule timezone if it exists */\n\tschedule_timezone: string;\n\t/** Last run timestamp in ms. */\n\tlast_run_at?: I64;\n\t/** Next scheduled run time in unix ms. */\n\tnext_scheduled_run?: I64;\n\t/**\n\t * If there is an error parsing schedule expression,\n\t * it will be given here.\n\t */\n\tschedule_error?: string;\n\t/** Resource tags. */\n\ttags: string[];\n}\n\nexport type ListSchedulesResponse = Schedule[];\n\nexport type ListSecretsResponse = string[];\n\nexport enum ServerState {\n\t/** Server health check passing. */\n\tOk = \"Ok\",\n\t/** Server is unreachable. */\n\tNotOk = \"NotOk\",\n\t/** Server is disabled. */\n\tDisabled = \"Disabled\",\n}\n\nexport interface ServerListItemInfo {\n\t/** The server's state. */\n\tstate: ServerState;\n\t/** Region of the server. */\n\tregion: string;\n\t/** Address of the server. */\n\taddress: string;\n\t/**\n\t * External address of the server (reachable by users).\n\t * Used with links.\n\t */\n\texternal_address?: string;\n\t/** The Komodo Periphery version of the server. */\n\tversion: string;\n\t/** Whether server is configured to send unreachable alerts. */\n\tsend_unreachable_alerts: boolean;\n\t/** Whether server is configured to send cpu alerts. */\n\tsend_cpu_alerts: boolean;\n\t/** Whether server is configured to send mem alerts. */\n\tsend_mem_alerts: boolean;\n\t/** Whether server is configured to send disk alerts. */\n\tsend_disk_alerts: boolean;\n\t/** Whether server is configured to send version mismatch alerts. */\n\tsend_version_mismatch_alerts: boolean;\n\t/** Whether terminals are disabled for this Server. */\n\tterminals_disabled: boolean;\n\t/** Whether container exec is disabled for this Server. */\n\tcontainer_exec_disabled: boolean;\n}\n\nexport type ServerListItem = ResourceListItem<ServerListItemInfo>;\n\nexport type ListServersResponse = ServerListItem[];\n\nexport interface StackService {\n\t/** The service name */\n\tservice: string;\n\t/** The service image */\n\timage: string;\n\t/** The container */\n\tcontainer?: ContainerListItem;\n\t/** Whether there is an update available for this services image. */\n\tupdate_available: boolean;\n}\n\nexport type ListStackServicesResponse = StackService[];\n\nexport enum StackState {\n\t/** The stack is currently re/deploying */\n\tDeploying = \"deploying\",\n\t/** All containers are running. */\n\tRunning = \"running\",\n\t/** All containers are paused */\n\tPaused = \"paused\",\n\t/** All contianers are stopped */\n\tStopped = \"stopped\",\n\t/** All containers are created */\n\tCreated = \"created\",\n\t/** All containers are restarting */\n\tRestarting = \"restarting\",\n\t/** All containers are dead */\n\tDead = \"dead\",\n\t/** All containers are removing */\n\tRemoving = \"removing\",\n\t/** The containers are in a mix of states */\n\tUnhealthy = \"unhealthy\",\n\t/** The stack is not deployed */\n\tDown = \"down\",\n\t/** Server not reachable for status */\n\tUnknown = \"unknown\",\n}\n\nexport interface StackServiceWithUpdate {\n\tservice: string;\n\t/** The service's image */\n\timage: string;\n\t/** Whether there is a newer image available for this service */\n\tupdate_available: boolean;\n}\n\nexport interface StackListItemInfo {\n\t/** The server that stack is deployed on. */\n\tserver_id: string;\n\t/** Whether stack is using files on host mode */\n\tfiles_on_host: boolean;\n\t/** Whether stack has file contents defined. */\n\tfile_contents: boolean;\n\t/** Linked repo, if one is attached. */\n\tlinked_repo: string;\n\t/** The git provider domain */\n\tgit_provider: string;\n\t/** The configured repo */\n\trepo: string;\n\t/** The configured branch */\n\tbranch: string;\n\t/** Full link to the repo. */\n\trepo_link: string;\n\t/** The stack state */\n\tstate: StackState;\n\t/** A string given by docker conveying the status of the stack. */\n\tstatus?: string;\n\t/**\n\t * The services that are part of the stack.\n\t * If deployed, will be `deployed_services`.\n\t * Otherwise, its `latest_services`\n\t */\n\tservices: StackServiceWithUpdate[];\n\t/**\n\t * Whether the compose project is missing on the host.\n\t * Ie, it does not show up in `docker compose ls`.\n\t * If true, and the stack is not Down, this is an unhealthy state.\n\t */\n\tproject_missing: boolean;\n\t/**\n\t * If any compose files are missing in the repo, the path will be here.\n\t * If there are paths here, this is an unhealthy state, and deploying will fail.\n\t */\n\tmissing_files: string[];\n\t/** Deployed short commit hash, or null. Only for repo based stacks. */\n\tdeployed_hash?: string;\n\t/** Latest short commit hash, or null. Only for repo based stacks */\n\tlatest_hash?: string;\n}\n\nexport type StackListItem = ResourceListItem<StackListItemInfo>;\n\nexport type ListStacksResponse = StackListItem[];\n\n/** Information about a process on the system. */\nexport interface SystemProcess {\n\t/** The process PID */\n\tpid: number;\n\t/** The process name */\n\tname: string;\n\t/** The path to the process executable */\n\texe?: string;\n\t/** The command used to start the process */\n\tcmd: string[];\n\t/** The time the process was started */\n\tstart_time?: number;\n\t/**\n\t * The cpu usage percentage of the process.\n\t * This is in core-percentage, eg 100% is 1 full core, and\n\t * an 8 core machine would max at 800%.\n\t */\n\tcpu_perc: number;\n\t/** The memory usage of the process in MB */\n\tmem_mb: number;\n\t/** Process disk read in KB/s */\n\tdisk_read_kb: number;\n\t/** Process disk write in KB/s */\n\tdisk_write_kb: number;\n}\n\nexport type ListSystemProcessesResponse = SystemProcess[];\n\nexport type ListTagsResponse = Tag[];\n\n/**\n * Info about an active terminal on a server.\n * Retrieve with [ListTerminals][crate::api::read::server::ListTerminals].\n */\nexport interface TerminalInfo {\n\t/** The name of the terminal. */\n\tname: string;\n\t/** The root program / args of the pty */\n\tcommand: string;\n\t/** The size of the terminal history in memory. */\n\tstored_size_kb: number;\n}\n\nexport type ListTerminalsResponse = TerminalInfo[];\n\nexport type ListUserGroupsResponse = UserGroup[];\n\nexport type ListUserTargetPermissionsResponse = Permission[];\n\nexport type ListUsersResponse = User[];\n\nexport type ListVariablesResponse = Variable[];\n\n/** The response for [LoginLocalUser] */\nexport type LoginLocalUserResponse = JwtResponse;\n\nexport type MongoDocument = any;\n\nexport interface ProcedureQuerySpecifics {\n}\n\nexport type ProcedureQuery = ResourceQuery<ProcedureQuerySpecifics>;\n\nexport type PushRecentlyViewedResponse = NoData;\n\nexport interface RepoQuerySpecifics {\n\t/** Filter repos by their repo. */\n\trepos: string[];\n}\n\nexport type RepoQuery = ResourceQuery<RepoQuerySpecifics>;\n\nexport interface ResourceSyncQuerySpecifics {\n\t/** Filter syncs by their repo. */\n\trepos: string[];\n}\n\nexport type ResourceSyncQuery = ResourceQuery<ResourceSyncQuerySpecifics>;\n\nexport type SearchContainerLogResponse = Log;\n\nexport type SearchDeploymentLogResponse = Log;\n\nexport type SearchStackLogResponse = Log;\n\nexport interface ServerQuerySpecifics {\n}\n\n/** Server-specific query */\nexport type ServerQuery = ResourceQuery<ServerQuerySpecifics>;\n\nexport type SetLastSeenUpdateResponse = NoData;\n\n/** Response for [SignUpLocalUser]. */\nexport type SignUpLocalUserResponse = JwtResponse;\n\nexport interface StackQuerySpecifics {\n\t/**\n\t * Query only for Stacks on these Servers.\n\t * If empty, does not filter by Server.\n\t * Only accepts Server id (not name).\n\t */\n\tserver_ids?: string[];\n\t/**\n\t * Query only for Stacks with these linked repos.\n\t * Only accepts Repo id (not name).\n\t */\n\tlinked_repos?: string[];\n\t/** Filter syncs by their repo. */\n\trepos?: string[];\n\t/** Query only for Stack with available image updates. */\n\tupdate_available?: boolean;\n}\n\nexport type StackQuery = ResourceQuery<StackQuerySpecifics>;\n\nexport type UpdateDockerRegistryAccountResponse = DockerRegistryAccount;\n\nexport type UpdateGitProviderAccountResponse = GitProviderAccount;\n\nexport type UpdatePermissionOnResourceTypeResponse = NoData;\n\nexport type UpdatePermissionOnTargetResponse = NoData;\n\nexport type UpdateProcedureResponse = Procedure;\n\nexport type UpdateResourceMetaResponse = NoData;\n\nexport type UpdateServiceUserDescriptionResponse = User;\n\nexport type UpdateUserAdminResponse = NoData;\n\nexport type UpdateUserBasePermissionsResponse = NoData;\n\nexport type UpdateUserPasswordResponse = NoData;\n\nexport type UpdateUserUsernameResponse = NoData;\n\nexport type UpdateVariableDescriptionResponse = Variable;\n\nexport type UpdateVariableIsSecretResponse = Variable;\n\nexport type UpdateVariableValueResponse = Variable;\n\nexport type _PartialActionConfig = Partial<ActionConfig>;\n\nexport type _PartialAlerterConfig = Partial<AlerterConfig>;\n\nexport type _PartialAwsBuilderConfig = Partial<AwsBuilderConfig>;\n\nexport type _PartialBuildConfig = Partial<BuildConfig>;\n\nexport type _PartialBuilderConfig = Partial<BuilderConfig>;\n\nexport type _PartialDeploymentConfig = Partial<DeploymentConfig>;\n\nexport type _PartialDockerRegistryAccount = Partial<DockerRegistryAccount>;\n\nexport type _PartialGitProviderAccount = Partial<GitProviderAccount>;\n\nexport type _PartialProcedureConfig = Partial<ProcedureConfig>;\n\nexport type _PartialRepoConfig = Partial<RepoConfig>;\n\nexport type _PartialResourceSyncConfig = Partial<ResourceSyncConfig>;\n\nexport type _PartialServerBuilderConfig = Partial<ServerBuilderConfig>;\n\nexport type _PartialServerConfig = Partial<ServerConfig>;\n\nexport type _PartialStackConfig = Partial<StackConfig>;\n\nexport type _PartialTag = Partial<Tag>;\n\nexport type _PartialUrlBuilderConfig = Partial<UrlBuilderConfig>;\n\nexport interface __Serror {\n\terror: string;\n\ttrace: string[];\n}\n\nexport type _Serror = __Serror;\n\n/** **Admin only.** Add a user to a user group. Response: [UserGroup] */\nexport interface AddUserToUserGroup {\n\t/** The name or id of UserGroup that user should be added to. */\n\tuser_group: string;\n\t/** The id or username of the user to add */\n\tuser: string;\n}\n\n/** Configuration for an AWS builder. */\nexport interface AwsBuilderConfig {\n\t/** The AWS region to create the instance in */\n\tregion: string;\n\t/** The instance type to create for the build */\n\tinstance_type: string;\n\t/** The size of the builder volume in gb */\n\tvolume_gb: number;\n\t/**\n\t * The port periphery will be running on.\n\t * Default: `8120`\n\t */\n\tport: number;\n\tuse_https: boolean;\n\t/**\n\t * The EC2 ami id to create.\n\t * The ami should have the periphery client configured to start on startup,\n\t * and should have the necessary github / dockerhub accounts configured.\n\t */\n\tami_id?: string;\n\t/** The subnet id to create the instance in. */\n\tsubnet_id?: string;\n\t/** The key pair name to attach to the instance */\n\tkey_pair_name?: string;\n\t/**\n\t * Whether to assign the instance a public IP address.\n\t * Likely needed for the instance to be able to reach the open internet.\n\t */\n\tassign_public_ip?: boolean;\n\t/**\n\t * Whether core should use the public IP address to communicate with periphery on the builder.\n\t * If false, core will communicate with the instance using the private IP.\n\t */\n\tuse_public_ip?: boolean;\n\t/**\n\t * The security group ids to attach to the instance.\n\t * This should include a security group to allow core inbound access to the periphery port.\n\t */\n\tsecurity_group_ids?: string[];\n\t/** The user data to deploy the instance with. */\n\tuser_data?: string;\n\t/** Which git providers are available on the AMI */\n\tgit_providers?: GitProvider[];\n\t/** Which docker registries are available on the AMI. */\n\tdocker_registries?: DockerRegistry[];\n\t/** Which secrets are available on the AMI. */\n\tsecrets?: string[];\n}\n\n/**\n * Backs up the Komodo Core database to compressed jsonl files.\n * Admin only. Response: [Update]\n * \n * Mount a folder to `/backups`, and Core will use it to create\n * timestamped database dumps, which can be restored using\n * the Komodo CLI.\n * \n * https://komo.do/docs/setup/backup\n */\nexport interface BackupCoreDatabase {\n}\n\n/** Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchBuildRepo {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* repos\n\t * foo-*\n\t * # add some more\n\t * extra-repo-1, extra-repo-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/** Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchCloneRepo {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* repos\n\t * foo-*\n\t * # add some more\n\t * extra-repo-1, extra-repo-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/** Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchDeploy {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* deployments\n\t * foo-*\n\t * # add some more\n\t * extra-deployment-1, extra-deployment-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchDeployStack {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* stacks\n\t * foo-*\n\t * # add some more\n\t * extra-stack-1, extra-stack-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchDeployStackIfChanged {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* stacks\n\t * foo-*\n\t * # add some more\n\t * extra-stack-1, extra-stack-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchDestroyDeployment {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* deployments\n\t * foo-*\n\t * # add some more\n\t * extra-deployment-1, extra-deployment-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchDestroyStack {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * d\n\t * Example:\n\t * ```text\n\t * # match all foo-* stacks\n\t * foo-*\n\t * # add some more\n\t * extra-stack-1, extra-stack-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\nexport interface BatchExecutionResponseItemErr {\n\tname: string;\n\terror: _Serror;\n}\n\n/** Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchPullRepo {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* repos\n\t * foo-*\n\t * # add some more\n\t * extra-repo-1, extra-repo-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/** Pulls multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchPullStack {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* stacks\n\t * foo-*\n\t * # add some more\n\t * extra-stack-1, extra-stack-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse] */\nexport interface BatchRunAction {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* actions\n\t * foo-*\n\t * # add some more\n\t * extra-action-1, extra-action-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchRunBuild {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* builds\n\t * foo-*\n\t * # add some more\n\t * extra-build-1, extra-build-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchRunProcedure {\n\t/**\n\t * Id or name or wildcard pattern or regex.\n\t * Supports multiline and comma delineated combinations of the above.\n\t * \n\t * Example:\n\t * ```text\n\t * # match all foo-* procedures\n\t * foo-*\n\t * # add some more\n\t * extra-procedure-1, extra-procedure-2\n\t * ```\n\t */\n\tpattern: string;\n}\n\n/**\n * Builds the target repo, using the attached builder. Response: [Update].\n * \n * Note. Repo must have builder attached at `builder_id`.\n * \n * 1. Spawns the target builder instance (For AWS type. For Server type, just use CloneRepo).\n * 2. Clones the repo on the builder using `git clone https://{$token?}@github.com/${repo} -b ${branch}`.\n * The token will only be used if a github account is specified,\n * and must be declared in the periphery configuration on the builder instance.\n * 3. If `on_clone` and `on_pull` are specified, they will be executed.\n * `on_clone` will be executed before `on_pull`.\n */\nexport interface BuildRepo {\n\t/** Id or name */\n\trepo: string;\n}\n\n/** Item in [GetBuildMonthlyStatsResponse] */\nexport interface BuildStatsDay {\n\ttime: number;\n\tcount: number;\n\tts: number;\n}\n\n/**\n * Cancels the target build.\n * Only does anything if the build is `building` when called.\n * Response: [Update]\n */\nexport interface CancelBuild {\n\t/** Can be id or name */\n\tbuild: string;\n}\n\n/**\n * Cancels the target repo build.\n * Only does anything if the repo build is `building` when called.\n * Response: [Update]\n */\nexport interface CancelRepoBuild {\n\t/** Can be id or name */\n\trepo: string;\n}\n\n/**\n * Clears all repos from the Core repo cache. Admin only.\n * Response: [Update]\n */\nexport interface ClearRepoCache {\n}\n\n/**\n * Clones the target repo. Response: [Update].\n * \n * Note. Repo must have server attached at `server_id`.\n * \n * 1. Clones the repo on the target server using `git clone https://{$token?}@github.com/${repo} -b ${branch}`.\n * The token will only be used if a github account is specified,\n * and must be declared in the periphery configuration on the target server.\n * 2. If `on_clone` and `on_pull` are specified, they will be executed.\n * `on_clone` will be executed before `on_pull`.\n */\nexport interface CloneRepo {\n\t/** Id or name */\n\trepo: string;\n}\n\n/**\n * Exports matching resources, and writes to the target sync's resource file. Response: [Update]\n * \n * Note. Will fail if the Sync is not `managed`.\n */\nexport interface CommitSync {\n\t/** Id or name */\n\tsync: string;\n}\n\n/**\n * Query to connect to a container exec session (interactive shell over websocket) on the given server.\n * TODO: Document calling.\n */\nexport interface ConnectContainerExecQuery {\n\t/** Server Id or name */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n\t/** The shell to use (eg. `sh` or `bash`) */\n\tshell: string;\n}\n\n/**\n * Query to connect to a container exec session (interactive shell over websocket) on the given Deployment.\n * This call will use access to the Deployment Terminal to permission the call.\n * TODO: Document calling.\n */\nexport interface ConnectDeploymentExecQuery {\n\t/** Deployment Id or name */\n\tdeployment: string;\n\t/** The shell to use (eg. `sh` or `bash`) */\n\tshell: string;\n}\n\n/**\n * Query to connect to a container exec session (interactive shell over websocket) on the given Stack / service.\n * This call will use access to the Stack Terminal to permission the call.\n * TODO: Document calling.\n */\nexport interface ConnectStackExecQuery {\n\t/** Stack Id or name */\n\tstack: string;\n\t/** The service name to connect to */\n\tservice: string;\n\t/** The shell to use (eg. `sh` or `bash`) */\n\tshell: string;\n}\n\n/**\n * Query to connect to a terminal (interactive shell over websocket) on the given server.\n * TODO: Document calling.\n */\nexport interface ConnectTerminalQuery {\n\t/** Server Id or name */\n\tserver: string;\n\t/**\n\t * Each periphery can keep multiple terminals open.\n\t * If a terminals with the specified name does not exist,\n\t * the call will fail.\n\t * Create a terminal using [CreateTerminal][super::write::server::CreateTerminal]\n\t */\n\tterminal: string;\n}\n\n/** Blkio stats entry.  This type is Linux-specific and omitted for Windows containers. */\nexport interface ContainerBlkioStatEntry {\n\tmajor?: U64;\n\tminor?: U64;\n\top?: string;\n\tvalue?: U64;\n}\n\n/**\n * BlkioStats stores all IO service stats for data read and write.\n * This type is Linux-specific and holds many fields that are specific to cgroups v1.\n * On a cgroup v2 host, all fields other than `io_service_bytes_recursive` are omitted or `null`.\n * This type is only populated on Linux and omitted for Windows containers.\n */\nexport interface ContainerBlkioStats {\n\tio_service_bytes_recursive?: ContainerBlkioStatEntry[];\n\t/**\n\t * This field is only available when using Linux containers with cgroups v1.\n\t * It is omitted or `null` when using cgroups v2.\n\t */\n\tio_serviced_recursive?: ContainerBlkioStatEntry[];\n\t/**\n\t * This field is only available when using Linux containers with cgroups v1.\n\t * It is omitted or `null` when using cgroups v2.\n\t */\n\tio_queue_recursive?: ContainerBlkioStatEntry[];\n\t/**\n\t * This field is only available when using Linux containers with cgroups v1.\n\t * It is omitted or `null` when using cgroups v2.\n\t */\n\tio_service_time_recursive?: ContainerBlkioStatEntry[];\n\t/**\n\t * This field is only available when using Linux containers with cgroups v1.\n\t * It is omitted or `null` when using cgroups v2.\n\t */\n\tio_wait_time_recursive?: ContainerBlkioStatEntry[];\n\t/**\n\t * This field is only available when using Linux containers with cgroups v1.\n\t * It is omitted or `null` when using cgroups v2.\n\t */\n\tio_merged_recursive?: ContainerBlkioStatEntry[];\n\t/**\n\t * This field is only available when using Linux containers with cgroups v1.\n\t * It is omitted or `null` when using cgroups v2.\n\t */\n\tio_time_recursive?: ContainerBlkioStatEntry[];\n\t/**\n\t * This field is only available when using Linux containers with cgroups v1.\n\t * It is omitted or `null` when using cgroups v2.\n\t */\n\tsectors_recursive?: ContainerBlkioStatEntry[];\n}\n\n/** All CPU stats aggregated since container inception. */\nexport interface ContainerCpuUsage {\n\t/** Total CPU time consumed in nanoseconds (Linux) or 100's of nanoseconds (Windows). */\n\ttotal_usage?: U64;\n\t/**\n\t * Total CPU time (in nanoseconds) consumed per core (Linux).\n\t * This field is Linux-specific when using cgroups v1.\n\t * It is omitted when using cgroups v2 and Windows containers.\n\t */\n\tpercpu_usage?: U64[];\n\t/**\n\t * Time (in nanoseconds) spent by tasks of the cgroup in kernel mode (Linux),\n\t * or time spent (in 100's of nanoseconds) by all container processes in kernel mode (Windows).\n\t * Not populated for Windows containers using Hyper-V isolation.\n\t */\n\tusage_in_kernelmode?: U64;\n\t/**\n\t * Time (in nanoseconds) spent by tasks of the cgroup in user mode (Linux),\n\t * or time spent (in 100's of nanoseconds) by all container processes in kernel mode (Windows).\n\t * Not populated for Windows containers using Hyper-V isolation.\n\t */\n\tusage_in_usermode?: U64;\n}\n\n/**\n * CPU throttling stats of the container.\n * This type is Linux-specific and omitted for Windows containers.\n */\nexport interface ContainerThrottlingData {\n\t/** Number of periods with throttling active. */\n\tperiods?: U64;\n\t/** Number of periods when the container hit its throttling limit. */\n\tthrottled_periods?: U64;\n\t/** Aggregated time (in nanoseconds) the container was throttled for. */\n\tthrottled_time?: U64;\n}\n\n/** CPU related info of the container */\nexport interface ContainerCpuStats {\n\t/** All CPU stats aggregated since container inception. */\n\tcpu_usage?: ContainerCpuUsage;\n\t/**\n\t * System Usage.\n\t * This field is Linux-specific and omitted for Windows containers.\n\t */\n\tsystem_cpu_usage?: U64;\n\t/**\n\t * Number of online CPUs.\n\t * This field is Linux-specific and omitted for Windows containers.\n\t */\n\tonline_cpus?: number;\n\t/**\n\t * CPU throttling stats of the container.\n\t * This type is Linux-specific and omitted for Windows containers.\n\t */\n\tthrottling_data?: ContainerThrottlingData;\n}\n\n/**\n * Aggregates all memory stats since container inception on Linux.\n * Windows returns stats for commit and private working set only.\n */\nexport interface ContainerMemoryStats {\n\t/**\n\t * Current `res_counter` usage for memory.\n\t * This field is Linux-specific and omitted for Windows containers.\n\t */\n\tusage?: U64;\n\t/**\n\t * Maximum usage ever recorded.\n\t * This field is Linux-specific and only supported on cgroups v1.\n\t * It is omitted when using cgroups v2 and for Windows containers.\n\t */\n\tmax_usage?: U64;\n\t/**\n\t * All the stats exported via memory.stat. when using cgroups v2.\n\t * This field is Linux-specific and omitted for Windows containers.\n\t */\n\tstats?: Record<string, U64>;\n\t/** Number of times memory usage hits limits.  This field is Linux-specific and only supported on cgroups v1. It is omitted when using cgroups v2 and for Windows containers. */\n\tfailcnt?: U64;\n\t/** This field is Linux-specific and omitted for Windows containers. */\n\tlimit?: U64;\n\t/**\n\t * Committed bytes.\n\t * This field is Windows-specific and omitted for Linux containers.\n\t */\n\tcommitbytes?: U64;\n\t/**\n\t * Peak committed bytes.\n\t * This field is Windows-specific and omitted for Linux containers.\n\t */\n\tcommitpeakbytes?: U64;\n\t/**\n\t * Private working set.\n\t * This field is Windows-specific and omitted for Linux containers.\n\t */\n\tprivateworkingset?: U64;\n}\n\n/** Aggregates the network stats of one container */\nexport interface ContainerNetworkStats {\n\t/** Bytes received. Windows and Linux. */\n\trx_bytes?: U64;\n\t/** Packets received. Windows and Linux. */\n\trx_packets?: U64;\n\t/**\n\t * Received errors. Not used on Windows.\n\t * This field is Linux-specific and always zero for Windows containers.\n\t */\n\trx_errors?: U64;\n\t/** Incoming packets dropped. Windows and Linux. */\n\trx_dropped?: U64;\n\t/** Bytes sent. Windows and Linux. */\n\ttx_bytes?: U64;\n\t/** Packets sent. Windows and Linux. */\n\ttx_packets?: U64;\n\t/**\n\t * Sent errors. Not used on Windows.\n\t * This field is Linux-specific and always zero for Windows containers.\n\t */\n\ttx_errors?: U64;\n\t/** Outgoing packets dropped. Windows and Linux. */\n\ttx_dropped?: U64;\n\t/**\n\t * Endpoint ID. Not used on Linux.\n\t * This field is Windows-specific and omitted for Linux containers.\n\t */\n\tendpoint_id?: string;\n\t/**\n\t * Instance ID. Not used on Linux.\n\t * This field is Windows-specific and omitted for Linux containers.\n\t */\n\tinstance_id?: string;\n}\n\n/** PidsStats contains Linux-specific stats of a container's process-IDs (PIDs).  This type is Linux-specific and omitted for Windows containers. */\nexport interface ContainerPidsStats {\n\t/** Current is the number of PIDs in the cgroup. */\n\tcurrent?: U64;\n\t/** Limit is the hard limit on the number of pids in the cgroup. A \\\"Limit\\\" of 0 means that there is no limit. */\n\tlimit?: U64;\n}\n\n/**\n * StorageStats is the disk I/O stats for read/write on Windows.\n * This type is Windows-specific and omitted for Linux containers.\n */\nexport interface ContainerStorageStats {\n\tread_count_normalized?: U64;\n\tread_size_bytes?: U64;\n\twrite_count_normalized?: U64;\n\twrite_size_bytes?: U64;\n}\n\nexport interface Conversion {\n\t/** reference on the server. */\n\tlocal: string;\n\t/** reference in the container. */\n\tcontainer: string;\n}\n\n/**\n * Creates a new action with given `name` and the configuration\n * of the action at the given `id`. Response: [Action].\n */\nexport interface CopyAction {\n\t/** The name of the new action. */\n\tname: string;\n\t/** The id of the action to copy. */\n\tid: string;\n}\n\n/**\n * Creates a new alerter with given `name` and the configuration\n * of the alerter at the given `id`. Response: [Alerter].\n */\nexport interface CopyAlerter {\n\t/** The name of the new alerter. */\n\tname: string;\n\t/** The id of the alerter to copy. */\n\tid: string;\n}\n\n/**\n * Creates a new build with given `name` and the configuration\n * of the build at the given `id`. Response: [Build].\n */\nexport interface CopyBuild {\n\t/** The name of the new build. */\n\tname: string;\n\t/** The id of the build to copy. */\n\tid: string;\n}\n\n/**\n * Creates a new builder with given `name` and the configuration\n * of the builder at the given `id`. Response: [Builder]\n */\nexport interface CopyBuilder {\n\t/** The name of the new builder. */\n\tname: string;\n\t/** The id of the builder to copy. */\n\tid: string;\n}\n\n/**\n * Creates a new deployment with given `name` and the configuration\n * of the deployment at the given `id`. Response: [Deployment]\n */\nexport interface CopyDeployment {\n\t/** The name of the new deployment. */\n\tname: string;\n\t/** The id of the deployment to copy. */\n\tid: string;\n}\n\n/**\n * Creates a new procedure with given `name` and the configuration\n * of the procedure at the given `id`. Response: [Procedure].\n */\nexport interface CopyProcedure {\n\t/** The name of the new procedure. */\n\tname: string;\n\t/** The id of the procedure to copy. */\n\tid: string;\n}\n\n/**\n * Creates a new repo with given `name` and the configuration\n * of the repo at the given `id`. Response: [Repo].\n */\nexport interface CopyRepo {\n\t/** The name of the new repo. */\n\tname: string;\n\t/** The id of the repo to copy. */\n\tid: string;\n}\n\n/**\n * Creates a new sync with given `name` and the configuration\n * of the sync at the given `id`. Response: [ResourceSync].\n */\nexport interface CopyResourceSync {\n\t/** The name of the new sync. */\n\tname: string;\n\t/** The id of the sync to copy. */\n\tid: string;\n}\n\n/**\n * Creates a new server with given `name` and the configuration\n * of the server at the given `id`. Response: [Server].\n */\nexport interface CopyServer {\n\t/** The name of the new server. */\n\tname: string;\n\t/** The id of the server to copy. */\n\tid: string;\n}\n\n/**\n * Creates a new stack with given `name` and the configuration\n * of the stack at the given `id`. Response: [Stack].\n */\nexport interface CopyStack {\n\t/** The name of the new stack. */\n\tname: string;\n\t/** The id of the stack to copy. */\n\tid: string;\n}\n\n/** Create a action. Response: [Action]. */\nexport interface CreateAction {\n\t/** The name given to newly created action. */\n\tname: string;\n\t/** Optional partial config to initialize the action with. */\n\tconfig?: _PartialActionConfig;\n}\n\n/**\n * Create a webhook on the github action attached to the Action resource.\n * passed in request. Response: [CreateActionWebhookResponse]\n */\nexport interface CreateActionWebhook {\n\t/** Id or name */\n\taction: string;\n}\n\n/** Create an alerter. Response: [Alerter]. */\nexport interface CreateAlerter {\n\t/** The name given to newly created alerter. */\n\tname: string;\n\t/** Optional partial config to initialize the alerter with. */\n\tconfig?: _PartialAlerterConfig;\n}\n\n/**\n * Create an api key for the calling user.\n * Response: [CreateApiKeyResponse].\n * \n * Note. After the response is served, there will be no way\n * to get the secret later.\n */\nexport interface CreateApiKey {\n\t/** The name for the api key. */\n\tname: string;\n\t/**\n\t * A unix timestamp in millseconds specifying api key expire time.\n\t * Default is 0, which means no expiry.\n\t */\n\texpires?: I64;\n}\n\n/**\n * Admin only method to create an api key for a service user.\n * Response: [CreateApiKeyResponse].\n */\nexport interface CreateApiKeyForServiceUser {\n\t/** Must be service user */\n\tuser_id: string;\n\t/** The name for the api key */\n\tname: string;\n\t/**\n\t * A unix timestamp in millseconds specifying api key expire time.\n\t * Default is 0, which means no expiry.\n\t */\n\texpires?: I64;\n}\n\n/** Create a build. Response: [Build]. */\nexport interface CreateBuild {\n\t/** The name given to newly created build. */\n\tname: string;\n\t/** Optional partial config to initialize the build with. */\n\tconfig?: _PartialBuildConfig;\n}\n\n/**\n * Create a webhook on the github repo attached to the build\n * passed in request. Response: [CreateBuildWebhookResponse]\n */\nexport interface CreateBuildWebhook {\n\t/** Id or name */\n\tbuild: string;\n}\n\n/** Partial representation of [BuilderConfig] */\nexport type PartialBuilderConfig = \n\t| { type: \"Url\", params: _PartialUrlBuilderConfig }\n\t| { type: \"Server\", params: _PartialServerBuilderConfig }\n\t| { type: \"Aws\", params: _PartialAwsBuilderConfig };\n\n/** Create a builder. Response: [Builder]. */\nexport interface CreateBuilder {\n\t/** The name given to newly created builder. */\n\tname: string;\n\t/** Optional partial config to initialize the builder with. */\n\tconfig?: PartialBuilderConfig;\n}\n\n/** Create a deployment. Response: [Deployment]. */\nexport interface CreateDeployment {\n\t/** The name given to newly created deployment. */\n\tname: string;\n\t/** Optional partial config to initialize the deployment with. */\n\tconfig?: _PartialDeploymentConfig;\n}\n\n/** Create a Deployment from an existing container. Response: [Deployment]. */\nexport interface CreateDeploymentFromContainer {\n\t/** The name or id of the existing container. */\n\tname: string;\n\t/** The server id or name on which container exists. */\n\tserver: string;\n}\n\n/**\n * **Admin only.** Create a docker registry account.\n * Response: [DockerRegistryAccount].\n */\nexport interface CreateDockerRegistryAccount {\n\taccount: _PartialDockerRegistryAccount;\n}\n\n/**\n * **Admin only.** Create a git provider account.\n * Response: [GitProviderAccount].\n */\nexport interface CreateGitProviderAccount {\n\t/**\n\t * The initial account config. Anything in the _id field will be ignored,\n\t * as this is generated on creation.\n\t */\n\taccount: _PartialGitProviderAccount;\n}\n\n/**\n * **Admin only.** Create a local user.\n * Response: [User].\n * \n * Note. Not to be confused with /auth/SignUpLocalUser.\n * This method requires admin user credentials, and can\n * bypass disabled user registration.\n */\nexport interface CreateLocalUser {\n\t/** The username for the local user. */\n\tusername: string;\n\t/** A password for the local user. */\n\tpassword: string;\n}\n\n/**\n * Create a docker network on the server.\n * Response: [Update]\n * \n * `docker network create {name}`\n */\nexport interface CreateNetwork {\n\t/** Server Id or name */\n\tserver: string;\n\t/** The name of the network to create. */\n\tname: string;\n}\n\n/** Create a procedure. Response: [Procedure]. */\nexport interface CreateProcedure {\n\t/** The name given to newly created build. */\n\tname: string;\n\t/** Optional partial config to initialize the procedure with. */\n\tconfig?: _PartialProcedureConfig;\n}\n\n/** Create a repo. Response: [Repo]. */\nexport interface CreateRepo {\n\t/** The name given to newly created repo. */\n\tname: string;\n\t/** Optional partial config to initialize the repo with. */\n\tconfig?: _PartialRepoConfig;\n}\n\nexport enum RepoWebhookAction {\n\tClone = \"Clone\",\n\tPull = \"Pull\",\n\tBuild = \"Build\",\n}\n\n/**\n * Create a webhook on the github repo attached to the (Komodo) Repo resource.\n * passed in request. Response: [CreateRepoWebhookResponse]\n */\nexport interface CreateRepoWebhook {\n\t/** Id or name */\n\trepo: string;\n\t/** \"Clone\" or \"Pull\" or \"Build\" */\n\taction: RepoWebhookAction;\n}\n\n/** Create a sync. Response: [ResourceSync]. */\nexport interface CreateResourceSync {\n\t/** The name given to newly created sync. */\n\tname: string;\n\t/** Optional partial config to initialize the sync with. */\n\tconfig?: _PartialResourceSyncConfig;\n}\n\n/** Create a server. Response: [Server]. */\nexport interface CreateServer {\n\t/** The name given to newly created server. */\n\tname: string;\n\t/** Optional partial config to initialize the server with. */\n\tconfig?: _PartialServerConfig;\n}\n\n/**\n * **Admin only.** Create a service user.\n * Response: [User].\n */\nexport interface CreateServiceUser {\n\t/** The username for the service user. */\n\tusername: string;\n\t/** A description for the service user. */\n\tdescription: string;\n}\n\n/** Create a stack. Response: [Stack]. */\nexport interface CreateStack {\n\t/** The name given to newly created stack. */\n\tname: string;\n\t/** Optional partial config to initialize the stack with. */\n\tconfig?: _PartialStackConfig;\n}\n\nexport enum StackWebhookAction {\n\tRefresh = \"Refresh\",\n\tDeploy = \"Deploy\",\n}\n\n/**\n * Create a webhook on the github repo attached to the stack\n * passed in request. Response: [CreateStackWebhookResponse]\n */\nexport interface CreateStackWebhook {\n\t/** Id or name */\n\tstack: string;\n\t/** \"Refresh\" or \"Deploy\" */\n\taction: StackWebhookAction;\n}\n\nexport enum SyncWebhookAction {\n\tRefresh = \"Refresh\",\n\tSync = \"Sync\",\n}\n\n/**\n * Create a webhook on the github repo attached to the sync\n * passed in request. Response: [CreateSyncWebhookResponse]\n */\nexport interface CreateSyncWebhook {\n\t/** Id or name */\n\tsync: string;\n\t/** \"Refresh\" or \"Sync\" */\n\taction: SyncWebhookAction;\n}\n\n/** Create a tag. Response: [Tag]. */\nexport interface CreateTag {\n\t/** The name of the tag. */\n\tname: string;\n\t/** Tag color. Default: Slate. */\n\tcolor?: TagColor;\n}\n\n/**\n * Configures the behavior of [CreateTerminal] if the\n * specified terminal name already exists.\n */\nexport enum TerminalRecreateMode {\n\t/**\n\t * Never kill the old terminal if it already exists.\n\t * If the command is different, returns error.\n\t */\n\tNever = \"Never\",\n\t/** Always kill the old terminal and create new one */\n\tAlways = \"Always\",\n\t/** Only kill and recreate if the command is different. */\n\tDifferentCommand = \"DifferentCommand\",\n}\n\n/**\n * Create a terminal on the server.\n * Response: [NoData]\n */\nexport interface CreateTerminal {\n\t/** Server Id or name */\n\tserver: string;\n\t/** The name of the terminal on the server to create. */\n\tname: string;\n\t/**\n\t * The shell command (eg `bash`) to init the shell.\n\t * \n\t * This can also include args:\n\t * `docker exec -it container sh`\n\t * \n\t * Default: `bash`\n\t */\n\tcommand: string;\n\t/** Default: `Never` */\n\trecreate?: TerminalRecreateMode;\n}\n\n/** **Admin only.** Create a user group. Response: [UserGroup] */\nexport interface CreateUserGroup {\n\t/** The name to assign to the new UserGroup */\n\tname: string;\n}\n\n/** **Admin only.** Create variable. Response: [Variable]. */\nexport interface CreateVariable {\n\t/** The name of the variable to create. */\n\tname: string;\n\t/** The initial value of the variable. defualt: \"\". */\n\tvalue?: string;\n\t/** The initial value of the description. default: \"\". */\n\tdescription?: string;\n\t/** Whether to make this a secret variable. */\n\tis_secret?: boolean;\n}\n\n/** Configuration for a Custom alerter endpoint. */\nexport interface CustomAlerterEndpoint {\n\t/** The http/s endpoint to send the POST to */\n\turl: string;\n}\n\n/**\n * Deletes the action at the given id, and returns the deleted action.\n * Response: [Action]\n */\nexport interface DeleteAction {\n\t/** The id or name of the action to delete. */\n\tid: string;\n}\n\n/**\n * Delete the webhook on the github action attached to the Action resource.\n * passed in request. Response: [DeleteActionWebhookResponse]\n */\nexport interface DeleteActionWebhook {\n\t/** Id or name */\n\taction: string;\n}\n\n/**\n * Deletes the alerter at the given id, and returns the deleted alerter.\n * Response: [Alerter]\n */\nexport interface DeleteAlerter {\n\t/** The id or name of the alerter to delete. */\n\tid: string;\n}\n\n/**\n * Delete all terminals on the server.\n * Response: [NoData]\n */\nexport interface DeleteAllTerminals {\n\t/** Server Id or name */\n\tserver: string;\n}\n\n/**\n * Delete an api key for the calling user.\n * Response: [NoData]\n */\nexport interface DeleteApiKey {\n\t/** The key which the user intends to delete. */\n\tkey: string;\n}\n\n/**\n * Admin only method to delete an api key for a service user.\n * Response: [NoData].\n */\nexport interface DeleteApiKeyForServiceUser {\n\tkey: string;\n}\n\n/**\n * Deletes the build at the given id, and returns the deleted build.\n * Response: [Build]\n */\nexport interface DeleteBuild {\n\t/** The id or name of the build to delete. */\n\tid: string;\n}\n\n/**\n * Delete a webhook on the github repo attached to the build\n * passed in request. Response: [CreateBuildWebhookResponse]\n */\nexport interface DeleteBuildWebhook {\n\t/** Id or name */\n\tbuild: string;\n}\n\n/**\n * Deletes the builder at the given id, and returns the deleted builder.\n * Response: [Builder]\n */\nexport interface DeleteBuilder {\n\t/** The id or name of the builder to delete. */\n\tid: string;\n}\n\n/**\n * Deletes the deployment at the given id, and returns the deleted deployment.\n * Response: [Deployment].\n * \n * Note. If the associated container is running, it will be deleted as part of\n * the deployment clean up.\n */\nexport interface DeleteDeployment {\n\t/** The id or name of the deployment to delete. */\n\tid: string;\n}\n\n/**\n * **Admin only.** Delete a docker registry account.\n * Response: [DockerRegistryAccount].\n */\nexport interface DeleteDockerRegistryAccount {\n\t/** The id of the docker registry account to delete */\n\tid: string;\n}\n\n/**\n * **Admin only.** Delete a git provider account.\n * Response: [DeleteGitProviderAccountResponse].\n */\nexport interface DeleteGitProviderAccount {\n\t/** The id of the git provider to delete */\n\tid: string;\n}\n\n/**\n * Delete a docker image.\n * Response: [Update]\n */\nexport interface DeleteImage {\n\t/** Id or name. */\n\tserver: string;\n\t/** The name of the image to delete. */\n\tname: string;\n}\n\n/**\n * Delete a docker network.\n * Response: [Update]\n */\nexport interface DeleteNetwork {\n\t/** Id or name. */\n\tserver: string;\n\t/** The name of the network to delete. */\n\tname: string;\n}\n\n/**\n * Deletes the procedure at the given id, and returns the deleted procedure.\n * Response: [Procedure]\n */\nexport interface DeleteProcedure {\n\t/** The id or name of the procedure to delete. */\n\tid: string;\n}\n\n/**\n * Deletes the repo at the given id, and returns the deleted repo.\n * Response: [Repo]\n */\nexport interface DeleteRepo {\n\t/** The id or name of the repo to delete. */\n\tid: string;\n}\n\n/**\n * Delete the webhook on the github repo attached to the (Komodo) Repo resource.\n * passed in request. Response: [DeleteRepoWebhookResponse]\n */\nexport interface DeleteRepoWebhook {\n\t/** Id or name */\n\trepo: string;\n\t/** \"Clone\" or \"Pull\" or \"Build\" */\n\taction: RepoWebhookAction;\n}\n\n/**\n * Deletes the sync at the given id, and returns the deleted sync.\n * Response: [ResourceSync]\n */\nexport interface DeleteResourceSync {\n\t/** The id or name of the sync to delete. */\n\tid: string;\n}\n\n/**\n * Deletes the server at the given id, and returns the deleted server.\n * Response: [Server]\n */\nexport interface DeleteServer {\n\t/** The id or name of the server to delete. */\n\tid: string;\n}\n\n/**\n * Deletes the stack at the given id, and returns the deleted stack.\n * Response: [Stack]\n */\nexport interface DeleteStack {\n\t/** The id or name of the stack to delete. */\n\tid: string;\n}\n\n/**\n * Delete the webhook on the github repo attached to the stack\n * passed in request. Response: [DeleteStackWebhookResponse]\n */\nexport interface DeleteStackWebhook {\n\t/** Id or name */\n\tstack: string;\n\t/** \"Refresh\" or \"Deploy\" */\n\taction: StackWebhookAction;\n}\n\n/**\n * Delete the webhook on the github repo attached to the sync\n * passed in request. Response: [DeleteSyncWebhookResponse]\n */\nexport interface DeleteSyncWebhook {\n\t/** Id or name */\n\tsync: string;\n\t/** \"Refresh\" or \"Sync\" */\n\taction: SyncWebhookAction;\n}\n\n/**\n * Delete a tag, and return the deleted tag. Response: [Tag].\n * \n * Note. Will also remove this tag from all attached resources.\n */\nexport interface DeleteTag {\n\t/** The id of the tag to delete. */\n\tid: string;\n}\n\n/**\n * Delete a terminal on the server.\n * Response: [NoData]\n */\nexport interface DeleteTerminal {\n\t/** Server Id or name */\n\tserver: string;\n\t/** The name of the terminal on the server to delete. */\n\tterminal: string;\n}\n\n/**\n * **Admin only**. Delete a user.\n * Admins can delete any non-admin user.\n * Only Super Admin can delete an admin.\n * No users can delete a Super Admin user.\n * User cannot delete themselves.\n * Response: [NoData].\n */\nexport interface DeleteUser {\n\t/** User id or username */\n\tuser: string;\n}\n\n/** **Admin only.** Delete a user group. Response: [UserGroup] */\nexport interface DeleteUserGroup {\n\t/** The id of the UserGroup */\n\tid: string;\n}\n\n/** **Admin only.** Delete a variable. Response: [Variable]. */\nexport interface DeleteVariable {\n\tname: string;\n}\n\n/**\n * Delete a docker volume.\n * Response: [Update]\n */\nexport interface DeleteVolume {\n\t/** Id or name. */\n\tserver: string;\n\t/** The name of the volume to delete. */\n\tname: string;\n}\n\n/**\n * Deploys the container for the target deployment. Response: [Update].\n * \n * 1. Pulls the image onto the target server.\n * 2. If the container is already running,\n * it will be stopped and removed using `docker container rm ${container_name}`.\n * 3. The container will be run using `docker run {...params}`,\n * where params are determined by the deployment's configuration.\n */\nexport interface Deploy {\n\t/** Name or id */\n\tdeployment: string;\n\t/**\n\t * Override the default termination signal specified in the deployment.\n\t * Only used when deployment needs to be taken down before redeploy.\n\t */\n\tstop_signal?: TerminationSignal;\n\t/**\n\t * Override the default termination max time.\n\t * Only used when deployment needs to be taken down before redeploy.\n\t */\n\tstop_time?: number;\n}\n\n/** Deploys the target stack. `docker compose up`. Response: [Update] */\nexport interface DeployStack {\n\t/** Id or name */\n\tstack: string;\n\t/**\n\t * Filter to only deploy specific services.\n\t * If empty, will deploy all services.\n\t */\n\tservices?: string[];\n\t/**\n\t * Override the default termination max time.\n\t * Only used if the stack needs to be taken down first.\n\t */\n\tstop_time?: number;\n}\n\n/**\n * Checks deployed contents vs latest contents,\n * and only if any changes found\n * will `docker compose up`. Response: [Update]\n */\nexport interface DeployStackIfChanged {\n\t/** Id or name */\n\tstack: string;\n\t/**\n\t * Override the default termination max time.\n\t * Only used if the stack needs to be taken down first.\n\t */\n\tstop_time?: number;\n}\n\n/**\n * Stops and destroys the container on the target server.\n * Reponse: [Update].\n * \n * 1. The container is stopped and removed using `docker container rm ${container_name}`.\n */\nexport interface DestroyContainer {\n\t/** Name or id */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n\t/** Override the default termination signal. */\n\tsignal?: TerminationSignal;\n\t/** Override the default termination max time. */\n\ttime?: number;\n}\n\n/**\n * Stops and destroys the container for the target deployment.\n * Reponse: [Update].\n * \n * 1. The container is stopped and removed using `docker container rm ${container_name}`.\n */\nexport interface DestroyDeployment {\n\t/** Name or id. */\n\tdeployment: string;\n\t/** Override the default termination signal specified in the deployment. */\n\tsignal?: TerminationSignal;\n\t/** Override the default termination max time. */\n\ttime?: number;\n}\n\n/** Destoys the target stack. `docker compose down`. Response: [Update] */\nexport interface DestroyStack {\n\t/** Id or name */\n\tstack: string;\n\t/**\n\t * Filter to only destroy specific services.\n\t * If empty, will destroy all services.\n\t */\n\tservices?: string[];\n\t/** Pass `--remove-orphans` */\n\tremove_orphans?: boolean;\n\t/** Override the default termination max time. */\n\tstop_time?: number;\n}\n\n/** Configuration for a Discord alerter. */\nexport interface DiscordAlerterEndpoint {\n\t/** The Discord webhook url */\n\turl: string;\n}\n\nexport interface EnvironmentVar {\n\tvariable: string;\n\tvalue: string;\n}\n\n/**\n * Exchange a single use exchange token (safe for transport in url query)\n * for a jwt.\n * Response: [ExchangeForJwtResponse].\n */\nexport interface ExchangeForJwt {\n\t/** The 'exchange token' */\n\ttoken: string;\n}\n\n/**\n * Execute a command in the given containers shell.\n * TODO: Document calling.\n */\nexport interface ExecuteContainerExecBody {\n\t/** Server Id or name */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n\t/** The shell to use (eg. `sh` or `bash`) */\n\tshell: string;\n\t/** The command to execute. */\n\tcommand: string;\n}\n\n/**\n * Execute a command in the given containers shell.\n * TODO: Document calling.\n */\nexport interface ExecuteDeploymentExecBody {\n\t/** Deployment Id or name */\n\tdeployment: string;\n\t/** The shell to use (eg. `sh` or `bash`) */\n\tshell: string;\n\t/** The command to execute. */\n\tcommand: string;\n}\n\n/**\n * Execute a command in the given containers shell.\n * TODO: Document calling.\n */\nexport interface ExecuteStackExecBody {\n\t/** Stack Id or name */\n\tstack: string;\n\t/** The service name to connect to */\n\tservice: string;\n\t/** The shell to use (eg. `sh` or `bash`) */\n\tshell: string;\n\t/** The command to execute. */\n\tcommand: string;\n}\n\n/**\n * Execute a terminal command on the given server.\n * TODO: Document calling.\n */\nexport interface ExecuteTerminalBody {\n\t/** Server Id or name */\n\tserver: string;\n\t/**\n\t * The name of the terminal on the server to use to execute.\n\t * If the terminal at name exists, it will be used to execute the command.\n\t * Otherwise, a new terminal will be created for this command, which will\n\t * persist until it exits or is deleted.\n\t */\n\tterminal: string;\n\t/** The command to execute. */\n\tcommand: string;\n}\n\n/**\n * Get pretty formatted monrun sync toml for all resources\n * which the user has permissions to view.\n * Response: [TomlResponse].\n */\nexport interface ExportAllResourcesToToml {\n\t/**\n\t * Whether to include any resources (servers, stacks, etc.)\n\t * in the exported contents.\n\t * Default: `true`\n\t */\n\tinclude_resources: boolean;\n\t/**\n\t * Filter resources by tag.\n\t * Accepts tag name or id. Empty array will not filter by tag.\n\t */\n\ttags?: string[];\n\t/**\n\t * Whether to include variables in the exported contents.\n\t * Default: false\n\t */\n\tinclude_variables?: boolean;\n\t/**\n\t * Whether to include user groups in the exported contents.\n\t * Default: false\n\t */\n\tinclude_user_groups?: boolean;\n}\n\n/**\n * Get pretty formatted monrun sync toml for specific resources and user groups.\n * Response: [TomlResponse].\n */\nexport interface ExportResourcesToToml {\n\t/** The targets to include in the export. */\n\ttargets?: ResourceTarget[];\n\t/** The user group names or ids to include in the export. */\n\tuser_groups?: string[];\n\t/** Whether to include variables */\n\tinclude_variables?: boolean;\n}\n\n/**\n * **Admin only.**\n * Find a user.\n * Response: [FindUserResponse]\n */\nexport interface FindUser {\n\t/** Id or username */\n\tuser: string;\n}\n\n/** Statistics sample for a container. */\nexport interface FullContainerStats {\n\t/** Name of the container */\n\tname: string;\n\t/** ID of the container */\n\tid?: string;\n\t/**\n\t * Date and time at which this sample was collected.\n\t * The value is formatted as [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) with nano-seconds.\n\t */\n\tread?: string;\n\t/**\n\t * Date and time at which this first sample was collected.\n\t * This field is not propagated if the \\\"one-shot\\\" option is set.\n\t * If the \\\"one-shot\\\" option is set, this field may be omitted, empty,\n\t * or set to a default date (`0001-01-01T00:00:00Z`).\n\t * The value is formatted as [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) with nano-seconds.\n\t */\n\tpreread?: string;\n\t/**\n\t * PidsStats contains Linux-specific stats of a container's process-IDs (PIDs).\n\t * This type is Linux-specific and omitted for Windows containers.\n\t */\n\tpids_stats?: ContainerPidsStats;\n\t/**\n\t * BlkioStats stores all IO service stats for data read and write.\n\t * This type is Linux-specific and holds many fields that are specific to cgroups v1.\n\t * On a cgroup v2 host, all fields other than `io_service_bytes_recursive` are omitted or `null`.\n\t * This type is only populated on Linux and omitted for Windows containers.\n\t */\n\tblkio_stats?: ContainerBlkioStats;\n\t/**\n\t * The number of processors on the system.\n\t * This field is Windows-specific and always zero for Linux containers.\n\t */\n\tnum_procs?: number;\n\tstorage_stats?: ContainerStorageStats;\n\tcpu_stats?: ContainerCpuStats;\n\tprecpu_stats?: ContainerCpuStats;\n\tmemory_stats?: ContainerMemoryStats;\n\t/** Network statistics for the container per interface.  This field is omitted if the container has no networking enabled. */\n\tnetworks?: Record<string, ContainerNetworkStats>;\n}\n\n/** Get a specific action. Response: [Action]. */\nexport interface GetAction {\n\t/** Id or name */\n\taction: string;\n}\n\n/** Get current action state for the action. Response: [ActionActionState]. */\nexport interface GetActionActionState {\n\t/** Id or name */\n\taction: string;\n}\n\n/**\n * Gets a summary of data relating to all actions.\n * Response: [GetActionsSummaryResponse].\n */\nexport interface GetActionsSummary {\n}\n\n/** Response for [GetActionsSummary]. */\nexport interface GetActionsSummaryResponse {\n\t/** The total number of actions. */\n\ttotal: number;\n\t/** The number of actions with Ok state. */\n\tok: number;\n\t/** The number of actions currently running. */\n\trunning: number;\n\t/** The number of actions with failed state. */\n\tfailed: number;\n\t/** The number of actions with unknown state. */\n\tunknown: number;\n}\n\n/** Get an alert: Response: [Alert]. */\nexport interface GetAlert {\n\tid: string;\n}\n\n/** Get a specific alerter. Response: [Alerter]. */\nexport interface GetAlerter {\n\t/** Id or name */\n\talerter: string;\n}\n\n/**\n * Gets a summary of data relating to all alerters.\n * Response: [GetAlertersSummaryResponse].\n */\nexport interface GetAlertersSummary {\n}\n\n/** Response for [GetAlertersSummary]. */\nexport interface GetAlertersSummaryResponse {\n\ttotal: number;\n}\n\n/** Get a specific build. Response: [Build]. */\nexport interface GetBuild {\n\t/** Id or name */\n\tbuild: string;\n}\n\n/** Get current action state for the build. Response: [BuildActionState]. */\nexport interface GetBuildActionState {\n\t/** Id or name */\n\tbuild: string;\n}\n\n/**\n * Gets summary and timeseries breakdown of the last months build count / time for charting.\n * Response: [GetBuildMonthlyStatsResponse].\n * \n * Note. This method is paginated. One page is 30 days of data.\n * Query for older pages by incrementing the page, starting at 0.\n */\nexport interface GetBuildMonthlyStats {\n\t/**\n\t * Query for older data by incrementing the page.\n\t * `page: 0` is the default, and will return the most recent data.\n\t */\n\tpage?: number;\n}\n\n/** Response for [GetBuildMonthlyStats]. */\nexport interface GetBuildMonthlyStatsResponse {\n\ttotal_time: number;\n\ttotal_count: number;\n\tdays: BuildStatsDay[];\n}\n\n/** Get whether a Build's target repo has a webhook for the build configured. Response: [GetBuildWebhookEnabledResponse]. */\nexport interface GetBuildWebhookEnabled {\n\t/** Id or name */\n\tbuild: string;\n}\n\n/** Response for [GetBuildWebhookEnabled] */\nexport interface GetBuildWebhookEnabledResponse {\n\t/**\n\t * Whether the repo webhooks can even be managed.\n\t * The repo owner must be in `github_webhook_app.owners` list to be managed.\n\t */\n\tmanaged: boolean;\n\t/** Whether pushes to branch trigger build. Will always be false if managed is false. */\n\tenabled: boolean;\n}\n\n/** Get a specific builder by id or name. Response: [Builder]. */\nexport interface GetBuilder {\n\t/** Id or name */\n\tbuilder: string;\n}\n\n/**\n * Gets a summary of data relating to all builders.\n * Response: [GetBuildersSummaryResponse].\n */\nexport interface GetBuildersSummary {\n}\n\n/** Response for [GetBuildersSummary]. */\nexport interface GetBuildersSummaryResponse {\n\t/** The total number of builders. */\n\ttotal: number;\n}\n\n/**\n * Gets a summary of data relating to all builds.\n * Response: [GetBuildsSummaryResponse].\n */\nexport interface GetBuildsSummary {\n}\n\n/** Response for [GetBuildsSummary]. */\nexport interface GetBuildsSummaryResponse {\n\t/** The total number of builds in Komodo. */\n\ttotal: number;\n\t/** The number of builds with Ok state. */\n\tok: number;\n\t/** The number of builds with Failed state. */\n\tfailed: number;\n\t/** The number of builds currently building. */\n\tbuilding: number;\n\t/** The number of builds with unknown state. */\n\tunknown: number;\n}\n\n/**\n * Get the container log's tail, split by stdout/stderr.\n * Response: [Log].\n * \n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface GetContainerLog {\n\t/** Id or name */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n\t/**\n\t * The number of lines of the log tail to include.\n\t * Default: 100.\n\t * Max: 5000.\n\t */\n\ttail: U64;\n\t/** Enable `--timestamps` */\n\ttimestamps?: boolean;\n}\n\n/**\n * Get info about the core api configuration.\n * Response: [GetCoreInfoResponse].\n */\nexport interface GetCoreInfo {\n}\n\n/** Response for [GetCoreInfo]. */\nexport interface GetCoreInfoResponse {\n\t/** The title assigned to this core api. */\n\ttitle: string;\n\t/** The monitoring interval of this core api. */\n\tmonitoring_interval: Timelength;\n\t/** The webhook base url. */\n\twebhook_base_url: string;\n\t/** Whether transparent mode is enabled, which gives all users read access to all resources. */\n\ttransparent_mode: boolean;\n\t/** Whether UI write access should be disabled */\n\tui_write_disabled: boolean;\n\t/** Whether non admins can create resources */\n\tdisable_non_admin_create: boolean;\n\t/** Whether confirm dialog should be disabled */\n\tdisable_confirm_dialog: boolean;\n\t/** The repo owners for which github webhook management api is available */\n\tgithub_webhook_owners: string[];\n\t/** Whether to disable websocket automatic reconnect. */\n\tdisable_websocket_reconnect: boolean;\n\t/** Whether to enable fancy toml highlighting. */\n\tenable_fancy_toml: boolean;\n\t/** TZ identifier Core is using, if manually set. */\n\ttimezone: string;\n}\n\n/** Get a specific deployment by name or id. Response: [Deployment]. */\nexport interface GetDeployment {\n\t/** Id or name */\n\tdeployment: string;\n}\n\n/**\n * Get current action state for the deployment.\n * Response: [DeploymentActionState].\n */\nexport interface GetDeploymentActionState {\n\t/** Id or name */\n\tdeployment: string;\n}\n\n/**\n * Get the container, including image / status, of the target deployment.\n * Response: [GetDeploymentContainerResponse].\n * \n * Note. This does not hit the server directly. The status comes from an\n * in memory cache on the core, which hits the server periodically\n * to keep it up to date.\n */\nexport interface GetDeploymentContainer {\n\t/** Id or name */\n\tdeployment: string;\n}\n\n/** Response for [GetDeploymentContainer]. */\nexport interface GetDeploymentContainerResponse {\n\tstate: DeploymentState;\n\tcontainer?: ContainerListItem;\n}\n\n/**\n * Get the deployment log's tail, split by stdout/stderr.\n * Response: [Log].\n * \n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface GetDeploymentLog {\n\t/** Id or name */\n\tdeployment: string;\n\t/**\n\t * The number of lines of the log tail to include.\n\t * Default: 100.\n\t * Max: 5000.\n\t */\n\ttail: U64;\n\t/** Enable `--timestamps` */\n\ttimestamps?: boolean;\n}\n\n/**\n * Get the deployment container's stats using `docker stats`.\n * Response: [GetDeploymentStatsResponse].\n * \n * Note. This call will hit the underlying server directly for most up to date stats.\n */\nexport interface GetDeploymentStats {\n\t/** Id or name */\n\tdeployment: string;\n}\n\n/**\n * Gets a summary of data relating to all deployments.\n * Response: [GetDeploymentsSummaryResponse].\n */\nexport interface GetDeploymentsSummary {\n}\n\n/** Response for [GetDeploymentsSummary]. */\nexport interface GetDeploymentsSummaryResponse {\n\t/** The total number of Deployments */\n\ttotal: I64;\n\t/** The number of Deployments with Running state */\n\trunning: I64;\n\t/** The number of Deployments with Stopped or Paused state */\n\tstopped: I64;\n\t/** The number of Deployments with NotDeployed state */\n\tnot_deployed: I64;\n\t/** The number of Deployments with Restarting or Dead or Created (other) state */\n\tunhealthy: I64;\n\t/** The number of Deployments with Unknown state */\n\tunknown: I64;\n}\n\n/**\n * Gets a summary of data relating to all containers.\n * Response: [GetDockerContainersSummaryResponse].\n */\nexport interface GetDockerContainersSummary {\n}\n\n/** Response for [GetDockerContainersSummary] */\nexport interface GetDockerContainersSummaryResponse {\n\t/** The total number of Containers */\n\ttotal: number;\n\t/** The number of Containers with Running state */\n\trunning: number;\n\t/** The number of Containers with Stopped or Paused or Created state */\n\tstopped: number;\n\t/** The number of Containers with Restarting or Dead state */\n\tunhealthy: number;\n\t/** The number of Containers with Unknown state */\n\tunknown: number;\n}\n\n/**\n * Get a specific docker registry account.\n * Response: [GetDockerRegistryAccountResponse].\n */\nexport interface GetDockerRegistryAccount {\n\tid: string;\n}\n\n/**\n * Get a specific git provider account.\n * Response: [GetGitProviderAccountResponse].\n */\nexport interface GetGitProviderAccount {\n\tid: string;\n}\n\n/**\n * Paginated endpoint serving historical (timeseries) server stats for graphing.\n * Response: [GetHistoricalServerStatsResponse].\n */\nexport interface GetHistoricalServerStats {\n\t/** Id or name */\n\tserver: string;\n\t/** The granularity of the data. */\n\tgranularity: Timelength;\n\t/**\n\t * Page of historical data. Default is 0, which is the most recent data.\n\t * Use with the `next_page` field of the response.\n\t */\n\tpage?: number;\n}\n\n/** System stats stored on the database. */\nexport interface SystemStatsRecord {\n\t/** Unix timestamp in milliseconds */\n\tts: I64;\n\t/** Server id */\n\tsid: string;\n\t/** Cpu usage percentage */\n\tcpu_perc: number;\n\t/** Load average (1m, 5m, 15m) */\n\tload_average?: SystemLoadAverage;\n\t/** Memory used in GB */\n\tmem_used_gb: number;\n\t/** Total memory in GB */\n\tmem_total_gb: number;\n\t/** Disk used in GB */\n\tdisk_used_gb: number;\n\t/** Total disk size in GB */\n\tdisk_total_gb: number;\n\t/** Breakdown of individual disks, including their usage, total size, and mount point */\n\tdisks: SingleDiskUsage[];\n\t/** Total network ingress in bytes */\n\tnetwork_ingress_bytes?: number;\n\t/** Total network egress in bytes */\n\tnetwork_egress_bytes?: number;\n}\n\n/** Response to [GetHistoricalServerStats]. */\nexport interface GetHistoricalServerStatsResponse {\n\t/** The timeseries page of data. */\n\tstats: SystemStatsRecord[];\n\t/** If there is a next page of data, pass this to `page` to get it. */\n\tnext_page?: number;\n}\n\n/**\n * Non authenticated route to see the available options\n * users have to login to Komodo, eg. local auth, github, google.\n * Response: [GetLoginOptionsResponse].\n */\nexport interface GetLoginOptions {\n}\n\n/** The response for [GetLoginOptions]. */\nexport interface GetLoginOptionsResponse {\n\t/** Whether local auth is enabled. */\n\tlocal: boolean;\n\t/** Whether github login is enabled. */\n\tgithub: boolean;\n\t/** Whether google login is enabled. */\n\tgoogle: boolean;\n\t/** Whether OIDC login is enabled. */\n\toidc: boolean;\n\t/** Whether user registration (Sign Up) has been disabled */\n\tregistration_disabled: boolean;\n}\n\n/**\n * Get the version of the Komodo Periphery agent on the target server.\n * Response: [GetPeripheryVersionResponse].\n */\nexport interface GetPeripheryVersion {\n\t/** Id or name */\n\tserver: string;\n}\n\n/** Response for [GetPeripheryVersion]. */\nexport interface GetPeripheryVersionResponse {\n\t/** The version of periphery. */\n\tversion: string;\n}\n\n/**\n * Gets the calling user's permission level on a specific resource.\n * Factors in any UserGroup's permissions they may be a part of.\n * Response: [PermissionLevel]\n */\nexport interface GetPermission {\n\t/** The target to get user permission on. */\n\ttarget: ResourceTarget;\n}\n\n/** Get a specific procedure. Response: [Procedure]. */\nexport interface GetProcedure {\n\t/** Id or name */\n\tprocedure: string;\n}\n\n/** Get current action state for the procedure. Response: [ProcedureActionState]. */\nexport interface GetProcedureActionState {\n\t/** Id or name */\n\tprocedure: string;\n}\n\n/**\n * Gets a summary of data relating to all procedures.\n * Response: [GetProceduresSummaryResponse].\n */\nexport interface GetProceduresSummary {\n}\n\n/** Response for [GetProceduresSummary]. */\nexport interface GetProceduresSummaryResponse {\n\t/** The total number of procedures. */\n\ttotal: number;\n\t/** The number of procedures with Ok state. */\n\tok: number;\n\t/** The number of procedures currently running. */\n\trunning: number;\n\t/** The number of procedures with failed state. */\n\tfailed: number;\n\t/** The number of procedures with unknown state. */\n\tunknown: number;\n}\n\n/** Get a specific repo. Response: [Repo]. */\nexport interface GetRepo {\n\t/** Id or name */\n\trepo: string;\n}\n\n/** Get current action state for the repo. Response: [RepoActionState]. */\nexport interface GetRepoActionState {\n\t/** Id or name */\n\trepo: string;\n}\n\n/** Get a target Repo's configured webhooks. Response: [GetRepoWebhooksEnabledResponse]. */\nexport interface GetRepoWebhooksEnabled {\n\t/** Id or name */\n\trepo: string;\n}\n\n/** Response for [GetRepoWebhooksEnabled] */\nexport interface GetRepoWebhooksEnabledResponse {\n\t/**\n\t * Whether the repo webhooks can even be managed.\n\t * The repo owner must be in `github_webhook_app.owners` list to be managed.\n\t */\n\tmanaged: boolean;\n\t/** Whether pushes to branch trigger clone. Will always be false if managed is false. */\n\tclone_enabled: boolean;\n\t/** Whether pushes to branch trigger pull. Will always be false if managed is false. */\n\tpull_enabled: boolean;\n\t/** Whether pushes to branch trigger build. Will always be false if managed is false. */\n\tbuild_enabled: boolean;\n}\n\n/**\n * Gets a summary of data relating to all repos.\n * Response: [GetReposSummaryResponse].\n */\nexport interface GetReposSummary {\n}\n\n/** Response for [GetReposSummary] */\nexport interface GetReposSummaryResponse {\n\t/** The total number of repos */\n\ttotal: number;\n\t/** The number of repos with Ok state. */\n\tok: number;\n\t/** The number of repos currently cloning. */\n\tcloning: number;\n\t/** The number of repos currently pulling. */\n\tpulling: number;\n\t/** The number of repos currently building. */\n\tbuilding: number;\n\t/** The number of repos with failed state. */\n\tfailed: number;\n\t/** The number of repos with unknown state. */\n\tunknown: number;\n}\n\n/** Find the attached resource for a container. Either Deployment or Stack. Response: [GetResourceMatchingContainerResponse]. */\nexport interface GetResourceMatchingContainer {\n\t/** Id or name */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n}\n\n/** Response for [GetResourceMatchingContainer]. Resource is either Deployment, Stack, or None. */\nexport interface GetResourceMatchingContainerResponse {\n\tresource?: ResourceTarget;\n}\n\n/** Get a specific sync. Response: [ResourceSync]. */\nexport interface GetResourceSync {\n\t/** Id or name */\n\tsync: string;\n}\n\n/** Get current action state for the sync. Response: [ResourceSyncActionState]. */\nexport interface GetResourceSyncActionState {\n\t/** Id or name */\n\tsync: string;\n}\n\n/**\n * Gets a summary of data relating to all syncs.\n * Response: [GetResourceSyncsSummaryResponse].\n */\nexport interface GetResourceSyncsSummary {\n}\n\n/** Response for [GetResourceSyncsSummary] */\nexport interface GetResourceSyncsSummaryResponse {\n\t/** The total number of syncs */\n\ttotal: number;\n\t/** The number of syncs with Ok state. */\n\tok: number;\n\t/** The number of syncs currently syncing. */\n\tsyncing: number;\n\t/** The number of syncs with pending updates */\n\tpending: number;\n\t/** The number of syncs with failed state. */\n\tfailed: number;\n\t/** The number of syncs with unknown state. */\n\tunknown: number;\n}\n\n/** Get a specific server. Response: [Server]. */\nexport interface GetServer {\n\t/** Id or name */\n\tserver: string;\n}\n\n/** Get current action state for the servers. Response: [ServerActionState]. */\nexport interface GetServerActionState {\n\t/** Id or name */\n\tserver: string;\n}\n\n/** Get the state of the target server. Response: [GetServerStateResponse]. */\nexport interface GetServerState {\n\t/** Id or name */\n\tserver: string;\n}\n\n/** The response for [GetServerState]. */\nexport interface GetServerStateResponse {\n\t/** The server status. */\n\tstatus: ServerState;\n}\n\n/**\n * Gets a summary of data relating to all servers.\n * Response: [GetServersSummaryResponse].\n */\nexport interface GetServersSummary {\n}\n\n/** Response for [GetServersSummary]. */\nexport interface GetServersSummaryResponse {\n\t/** The total number of servers. */\n\ttotal: I64;\n\t/** The number of healthy (`status: OK`) servers. */\n\thealthy: I64;\n\t/** The number of servers with warnings (e.g., version mismatch). */\n\twarning: I64;\n\t/** The number of unhealthy servers. */\n\tunhealthy: I64;\n\t/** The number of disabled servers. */\n\tdisabled: I64;\n}\n\n/** Get a specific stack. Response: [Stack]. */\nexport interface GetStack {\n\t/** Id or name */\n\tstack: string;\n}\n\n/** Get current action state for the stack. Response: [StackActionState]. */\nexport interface GetStackActionState {\n\t/** Id or name */\n\tstack: string;\n}\n\n/**\n * Get a stack's logs. Filter down included services. Response: [GetStackLogResponse].\n * \n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface GetStackLog {\n\t/** Id or name */\n\tstack: string;\n\t/**\n\t * Filter the logs to only ones from specific services.\n\t * If empty, will include logs from all services.\n\t */\n\tservices: string[];\n\t/**\n\t * The number of lines of the log tail to include.\n\t * Default: 100.\n\t * Max: 5000.\n\t */\n\ttail: U64;\n\t/** Enable `--timestamps` */\n\ttimestamps?: boolean;\n}\n\n/** Get a target stack's configured webhooks. Response: [GetStackWebhooksEnabledResponse]. */\nexport interface GetStackWebhooksEnabled {\n\t/** Id or name */\n\tstack: string;\n}\n\n/** Response for [GetStackWebhooksEnabled] */\nexport interface GetStackWebhooksEnabledResponse {\n\t/**\n\t * Whether the repo webhooks can even be managed.\n\t * The repo owner must be in `github_webhook_app.owners` list to be managed.\n\t */\n\tmanaged: boolean;\n\t/** Whether pushes to branch trigger refresh. Will always be false if managed is false. */\n\trefresh_enabled: boolean;\n\t/** Whether pushes to branch trigger stack execution. Will always be false if managed is false. */\n\tdeploy_enabled: boolean;\n}\n\n/**\n * Gets a summary of data relating to all syncs.\n * Response: [GetStacksSummaryResponse].\n */\nexport interface GetStacksSummary {\n}\n\n/** Response for [GetStacksSummary] */\nexport interface GetStacksSummaryResponse {\n\t/** The total number of stacks */\n\ttotal: number;\n\t/** The number of stacks with Running state. */\n\trunning: number;\n\t/** The number of stacks with Stopped or Paused state. */\n\tstopped: number;\n\t/** The number of stacks with Down state. */\n\tdown: number;\n\t/** The number of stacks with Unhealthy or Restarting or Dead or Created or Removing state. */\n\tunhealthy: number;\n\t/** The number of stacks with Unknown state. */\n\tunknown: number;\n}\n\n/** Get a target Sync's configured webhooks. Response: [GetSyncWebhooksEnabledResponse]. */\nexport interface GetSyncWebhooksEnabled {\n\t/** Id or name */\n\tsync: string;\n}\n\n/** Response for [GetSyncWebhooksEnabled] */\nexport interface GetSyncWebhooksEnabledResponse {\n\t/**\n\t * Whether the repo webhooks can even be managed.\n\t * The repo owner must be in `github_webhook_app.owners` list to be managed.\n\t */\n\tmanaged: boolean;\n\t/** Whether pushes to branch trigger refresh. Will always be false if managed is false. */\n\trefresh_enabled: boolean;\n\t/** Whether pushes to branch trigger sync execution. Will always be false if managed is false. */\n\tsync_enabled: boolean;\n}\n\n/**\n * Get the system information of the target server.\n * Response: [SystemInformation].\n */\nexport interface GetSystemInformation {\n\t/** Id or name */\n\tserver: string;\n}\n\n/**\n * Get the system stats on the target server. Response: [SystemStats].\n * \n * Note. This does not hit the server directly. The stats come from an\n * in memory cache on the core, which hits the server periodically\n * to keep it up to date.\n */\nexport interface GetSystemStats {\n\t/** Id or name */\n\tserver: string;\n}\n\n/** Get data for a specific tag. Response [Tag]. */\nexport interface GetTag {\n\t/** Id or name */\n\ttag: string;\n}\n\n/**\n * Get all data for the target update.\n * Response: [Update].\n */\nexport interface GetUpdate {\n\t/** The update id. */\n\tid: string;\n}\n\n/**\n * Get the user extracted from the request headers.\n * Response: [User].\n */\nexport interface GetUser {\n}\n\n/**\n * Get a specific user group by name or id.\n * Response: [UserGroup].\n */\nexport interface GetUserGroup {\n\t/** Name or Id */\n\tuser_group: string;\n}\n\n/**\n * Gets the username of a specific user.\n * Response: [GetUsernameResponse]\n */\nexport interface GetUsername {\n\t/** The id of the user. */\n\tuser_id: string;\n}\n\n/** Response for [GetUsername]. */\nexport interface GetUsernameResponse {\n\t/** The username of the user. */\n\tusername: string;\n\t/** An optional icon for the user. */\n\tavatar?: string;\n}\n\n/**\n * List all available global variables.\n * Response: [Variable]\n * \n * Note. For non admin users making this call,\n * secret variables will have their values obscured.\n */\nexport interface GetVariable {\n\t/** The name of the variable to get. */\n\tname: string;\n}\n\n/**\n * Get the version of the Komodo Core api.\n * Response: [GetVersionResponse].\n */\nexport interface GetVersion {\n}\n\n/** Response for [GetVersion]. */\nexport interface GetVersionResponse {\n\t/** The version of the core api. */\n\tversion: string;\n}\n\n/**\n * Trigger a global poll for image updates on Stacks and Deployments\n * with `poll_for_updates` or `auto_update` enabled.\n * Admin only. Response: [Update]\n * \n * 1. `docker compose pull` any Stacks / Deployments with `poll_for_updates` or `auto_update` enabled. This will pick up any available updates.\n * 2. Redeploy Stacks / Deployments that have updates found and 'auto_update' enabled.\n */\nexport interface GlobalAutoUpdate {\n}\n\n/**\n * Inspect the docker container associated with the Deployment.\n * Response: [Container].\n */\nexport interface InspectDeploymentContainer {\n\t/** Id or name */\n\tdeployment: string;\n}\n\n/** Inspect a docker container on the server. Response: [Container]. */\nexport interface InspectDockerContainer {\n\t/** Id or name */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n}\n\n/** Inspect a docker image on the server. Response: [Image]. */\nexport interface InspectDockerImage {\n\t/** Id or name */\n\tserver: string;\n\t/** The image name */\n\timage: string;\n}\n\n/** Inspect a docker network on the server. Response: [InspectDockerNetworkResponse]. */\nexport interface InspectDockerNetwork {\n\t/** Id or name */\n\tserver: string;\n\t/** The network name */\n\tnetwork: string;\n}\n\n/** Inspect a docker volume on the server. Response: [Volume]. */\nexport interface InspectDockerVolume {\n\t/** Id or name */\n\tserver: string;\n\t/** The volume name */\n\tvolume: string;\n}\n\n/**\n * Inspect the docker container associated with the Stack.\n * Response: [Container].\n */\nexport interface InspectStackContainer {\n\t/** Id or name */\n\tstack: string;\n\t/** The service name to inspect */\n\tservice: string;\n}\n\nexport interface LatestCommit {\n\thash: string;\n\tmessage: string;\n}\n\n/** List actions matching optional query. Response: [ListActionsResponse]. */\nexport interface ListActions {\n\t/** optional structured query to filter actions. */\n\tquery?: ActionQuery;\n}\n\n/** List alerters matching optional query. Response: [ListAlertersResponse]. */\nexport interface ListAlerters {\n\t/** Structured query to filter alerters. */\n\tquery?: AlerterQuery;\n}\n\n/**\n * Get a paginated list of alerts sorted by timestamp descending.\n * Response: [ListAlertsResponse].\n */\nexport interface ListAlerts {\n\t/**\n\t * Pass a custom mongo query to filter the alerts.\n\t * \n\t * ## Example JSON\n\t * ```json\n\t * {\n\t * \"resolved\": \"false\",\n\t * \"level\": \"CRITICAL\",\n\t * \"$or\": [\n\t * {\n\t * \"target\": {\n\t * \"type\": \"Server\",\n\t * \"id\": \"6608bf89cb2a12b257ab6c09\"\n\t * }\n\t * },\n\t * {\n\t * \"target\": {\n\t * \"type\": \"Server\",\n\t * \"id\": \"660a5f60b74f90d5dae45fa3\"\n\t * }\n\t * }\n\t * ]\n\t * }\n\t * ```\n\t * This will filter to only include open alerts that have CRITICAL level on those two servers.\n\t */\n\tquery?: MongoDocument;\n\t/**\n\t * Retrieve older results by incrementing the page.\n\t * `page: 0` is default, and returns the most recent results.\n\t */\n\tpage?: U64;\n}\n\n/** Response for [ListAlerts]. */\nexport interface ListAlertsResponse {\n\talerts: Alert[];\n\t/**\n\t * If more alerts exist, the next page will be given here.\n\t * Otherwise it will be `null`\n\t */\n\tnext_page?: I64;\n}\n\n/**\n * List all docker containers on the target server.\n * Response: [ListDockerContainersResponse].\n */\nexport interface ListAllDockerContainers {\n\t/** Filter by server id or name. */\n\tservers?: string[];\n}\n\n/**\n * Gets list of api keys for the calling user.\n * Response: [ListApiKeysResponse]\n */\nexport interface ListApiKeys {\n}\n\n/**\n * **Admin only.**\n * Gets list of api keys for the user.\n * Will still fail if you call for a user_id that isn't a service user.\n * Response: [ListApiKeysForServiceUserResponse]\n */\nexport interface ListApiKeysForServiceUser {\n\t/** Id or username */\n\tuser: string;\n}\n\n/**\n * Retrieve versions of the build that were built in the past and available for deployment,\n * sorted by most recent first.\n * Response: [ListBuildVersionsResponse].\n */\nexport interface ListBuildVersions {\n\t/** Id or name */\n\tbuild: string;\n\t/** Filter to only include versions matching this major version. */\n\tmajor?: number;\n\t/** Filter to only include versions matching this minor version. */\n\tminor?: number;\n\t/** Filter to only include versions matching this patch version. */\n\tpatch?: number;\n\t/** Limit the number of included results. Default is no limit. */\n\tlimit?: I64;\n}\n\n/** List builders matching structured query. Response: [ListBuildersResponse]. */\nexport interface ListBuilders {\n\tquery?: BuilderQuery;\n}\n\n/** List builds matching optional query. Response: [ListBuildsResponse]. */\nexport interface ListBuilds {\n\t/** optional structured query to filter builds. */\n\tquery?: BuildQuery;\n}\n\n/**\n * Gets a list of existing values used as extra args across other builds.\n * Useful to offer suggestions. Response: [ListCommonBuildExtraArgsResponse]\n */\nexport interface ListCommonBuildExtraArgs {\n\t/** optional structured query to filter builds. */\n\tquery?: BuildQuery;\n}\n\n/**\n * Gets a list of existing values used as extra args across other deployments.\n * Useful to offer suggestions. Response: [ListCommonDeploymentExtraArgsResponse]\n */\nexport interface ListCommonDeploymentExtraArgs {\n\t/** optional structured query to filter deployments. */\n\tquery?: DeploymentQuery;\n}\n\n/**\n * Gets a list of existing values used as build extra args across other stacks.\n * Useful to offer suggestions. Response: [ListCommonStackBuildExtraArgsResponse]\n */\nexport interface ListCommonStackBuildExtraArgs {\n\t/** optional structured query to filter stacks. */\n\tquery?: StackQuery;\n}\n\n/**\n * Gets a list of existing values used as extra args across other stacks.\n * Useful to offer suggestions. Response: [ListCommonStackExtraArgsResponse]\n */\nexport interface ListCommonStackExtraArgs {\n\t/** optional structured query to filter stacks. */\n\tquery?: StackQuery;\n}\n\n/**\n * List all docker compose projects on the target server.\n * Response: [ListComposeProjectsResponse].\n */\nexport interface ListComposeProjects {\n\t/** Id or name */\n\tserver: string;\n}\n\n/**\n * List deployments matching optional query.\n * Response: [ListDeploymentsResponse].\n */\nexport interface ListDeployments {\n\t/** optional structured query to filter deployments. */\n\tquery?: DeploymentQuery;\n}\n\n/**\n * List all docker containers on the target server.\n * Response: [ListDockerContainersResponse].\n */\nexport interface ListDockerContainers {\n\t/** Id or name */\n\tserver: string;\n}\n\n/** Get image history from the server. Response: [ListDockerImageHistoryResponse]. */\nexport interface ListDockerImageHistory {\n\t/** Id or name */\n\tserver: string;\n\t/** The image name */\n\timage: string;\n}\n\n/**\n * List the docker images locally cached on the target server.\n * Response: [ListDockerImagesResponse].\n */\nexport interface ListDockerImages {\n\t/** Id or name */\n\tserver: string;\n}\n\n/** List the docker networks on the server. Response: [ListDockerNetworksResponse]. */\nexport interface ListDockerNetworks {\n\t/** Id or name */\n\tserver: string;\n}\n\n/**\n * List the docker registry providers available in Core / Periphery config files.\n * Response: [ListDockerRegistriesFromConfigResponse].\n * \n * Includes:\n * - registries in core config\n * - registries configured on builds, deployments\n * - registries on the optional Server or Builder\n */\nexport interface ListDockerRegistriesFromConfig {\n\t/**\n\t * Accepts an optional Server or Builder target to expand the core list with\n\t * providers available on that specific resource.\n\t */\n\ttarget?: ResourceTarget;\n}\n\n/**\n * List docker registry accounts matching optional query.\n * Response: [ListDockerRegistryAccountsResponse].\n */\nexport interface ListDockerRegistryAccounts {\n\t/** Optionally filter by accounts with a specific domain. */\n\tdomain?: string;\n\t/** Optionally filter by accounts with a specific username. */\n\tusername?: string;\n}\n\n/**\n * List all docker volumes on the target server.\n * Response: [ListDockerVolumesResponse].\n */\nexport interface ListDockerVolumes {\n\t/** Id or name */\n\tserver: string;\n}\n\n/** List actions matching optional query. Response: [ListFullActionsResponse]. */\nexport interface ListFullActions {\n\t/** optional structured query to filter actions. */\n\tquery?: ActionQuery;\n}\n\n/** List full alerters matching optional query. Response: [ListFullAlertersResponse]. */\nexport interface ListFullAlerters {\n\t/** Structured query to filter alerters. */\n\tquery?: AlerterQuery;\n}\n\n/** List builders matching structured query. Response: [ListFullBuildersResponse]. */\nexport interface ListFullBuilders {\n\tquery?: BuilderQuery;\n}\n\n/** List builds matching optional query. Response: [ListFullBuildsResponse]. */\nexport interface ListFullBuilds {\n\t/** optional structured query to filter builds. */\n\tquery?: BuildQuery;\n}\n\n/**\n * List deployments matching optional query.\n * Response: [ListFullDeploymentsResponse].\n */\nexport interface ListFullDeployments {\n\t/** optional structured query to filter deployments. */\n\tquery?: DeploymentQuery;\n}\n\n/** List procedures matching optional query. Response: [ListFullProceduresResponse]. */\nexport interface ListFullProcedures {\n\t/** optional structured query to filter procedures. */\n\tquery?: ProcedureQuery;\n}\n\n/** List repos matching optional query. Response: [ListFullReposResponse]. */\nexport interface ListFullRepos {\n\t/** optional structured query to filter repos. */\n\tquery?: RepoQuery;\n}\n\n/** List syncs matching optional query. Response: [ListFullResourceSyncsResponse]. */\nexport interface ListFullResourceSyncs {\n\t/** optional structured query to filter syncs. */\n\tquery?: ResourceSyncQuery;\n}\n\n/** List servers matching optional query. Response: [ListFullServersResponse]. */\nexport interface ListFullServers {\n\t/** optional structured query to filter servers. */\n\tquery?: ServerQuery;\n}\n\n/** List stacks matching optional query. Response: [ListFullStacksResponse]. */\nexport interface ListFullStacks {\n\t/** optional structured query to filter stacks. */\n\tquery?: StackQuery;\n}\n\n/**\n * List git provider accounts matching optional query.\n * Response: [ListGitProviderAccountsResponse].\n */\nexport interface ListGitProviderAccounts {\n\t/** Optionally filter by accounts with a specific domain. */\n\tdomain?: string;\n\t/** Optionally filter by accounts with a specific username. */\n\tusername?: string;\n}\n\n/**\n * List the git providers available in Core / Periphery config files.\n * Response: [ListGitProvidersFromConfigResponse].\n * \n * Includes:\n * - providers in core config\n * - providers configured on builds, repos, syncs\n * - providers on the optional Server or Builder\n */\nexport interface ListGitProvidersFromConfig {\n\t/**\n\t * Accepts an optional Server or Builder target to expand the core list with\n\t * providers available on that specific resource.\n\t */\n\ttarget?: ResourceTarget;\n}\n\n/**\n * List permissions for the calling user.\n * Does not include any permissions on UserGroups they may be a part of.\n * Response: [ListPermissionsResponse]\n */\nexport interface ListPermissions {\n}\n\n/** List procedures matching optional query. Response: [ListProceduresResponse]. */\nexport interface ListProcedures {\n\t/** optional structured query to filter procedures. */\n\tquery?: ProcedureQuery;\n}\n\n/** List repos matching optional query. Response: [ListReposResponse]. */\nexport interface ListRepos {\n\t/** optional structured query to filter repos. */\n\tquery?: RepoQuery;\n}\n\n/** List syncs matching optional query. Response: [ListResourceSyncsResponse]. */\nexport interface ListResourceSyncs {\n\t/** optional structured query to filter syncs. */\n\tquery?: ResourceSyncQuery;\n}\n\n/**\n * List configured schedules.\n * Response: [ListSchedulesResponse].\n */\nexport interface ListSchedules {\n\t/** Pass Vec of tag ids or tag names */\n\ttags?: string[];\n\t/** 'All' or 'Any' */\n\ttag_behavior?: TagQueryBehavior;\n}\n\n/**\n * List the available secrets from the core config.\n * Response: [ListSecretsResponse].\n */\nexport interface ListSecrets {\n\t/**\n\t * Accepts an optional Server or Builder target to expand the core list with\n\t * providers available on that specific resource.\n\t */\n\ttarget?: ResourceTarget;\n}\n\n/** List servers matching optional query. Response: [ListServersResponse]. */\nexport interface ListServers {\n\t/** optional structured query to filter servers. */\n\tquery?: ServerQuery;\n}\n\n/** Lists a specific stacks services (the containers). Response: [ListStackServicesResponse]. */\nexport interface ListStackServices {\n\t/** Id or name */\n\tstack: string;\n}\n\n/** List stacks matching optional query. Response: [ListStacksResponse]. */\nexport interface ListStacks {\n\t/** optional structured query to filter stacks. */\n\tquery?: StackQuery;\n}\n\n/**\n * List the processes running on the target server.\n * Response: [ListSystemProcessesResponse].\n * \n * Note. This does not hit the server directly. The procedures come from an\n * in memory cache on the core, which hits the server periodically\n * to keep it up to date.\n */\nexport interface ListSystemProcesses {\n\t/** Id or name */\n\tserver: string;\n}\n\n/**\n * List data for tags matching optional mongo query.\n * Response: [ListTagsResponse].\n */\nexport interface ListTags {\n\tquery?: MongoDocument;\n}\n\n/**\n * List the current terminals on specified server.\n * Response: [ListTerminalsResponse].\n */\nexport interface ListTerminals {\n\t/** Id or name */\n\tserver: string;\n\t/**\n\t * Force a fresh call to Periphery for the list.\n\t * Otherwise the response will be cached for 30s\n\t */\n\tfresh?: boolean;\n}\n\n/**\n * Paginated endpoint for updates matching optional query.\n * More recent updates will be returned first.\n */\nexport interface ListUpdates {\n\t/** An optional mongo query to filter the updates. */\n\tquery?: MongoDocument;\n\t/**\n\t * Page of updates. Default is 0, which is the most recent data.\n\t * Use with the `next_page` field of the response.\n\t */\n\tpage?: number;\n}\n\n/** Minimal representation of an action performed by Komodo. */\nexport interface UpdateListItem {\n\t/** The id of the update */\n\tid: string;\n\t/** Which operation was run */\n\toperation: Operation;\n\t/** The starting time of the operation */\n\tstart_ts: I64;\n\t/** Whether the operation was successful */\n\tsuccess: boolean;\n\t/** The username of the user performing update */\n\tusername: string;\n\t/**\n\t * The user id that triggered the update.\n\t * \n\t * Also can take these values for operations triggered automatically:\n\t * - `Procedure`: The operation was triggered as part of a procedure run\n\t * - `Github`: The operation was triggered by a github webhook\n\t * - `Auto Redeploy`: The operation (always `Deploy`) was triggered by an attached build finishing.\n\t */\n\toperator: string;\n\t/** The target resource to which this update refers */\n\ttarget: ResourceTarget;\n\t/**\n\t * The status of the update\n\t * - `Queued`\n\t * - `InProgress`\n\t * - `Complete`\n\t */\n\tstatus: UpdateStatus;\n\t/** An optional version on the update, ie build version or deployed version. */\n\tversion?: Version;\n\t/** Some unstructured, operation specific data. Not for general usage. */\n\tother_data?: string;\n}\n\n/** Response for [ListUpdates]. */\nexport interface ListUpdatesResponse {\n\t/** The page of updates, sorted by timestamp descending. */\n\tupdates: UpdateListItem[];\n\t/** If there is a next page of data, pass this to `page` to get it. */\n\tnext_page?: number;\n}\n\n/**\n * List all user groups which user can see. Response: [ListUserGroupsResponse].\n * \n * Admins can see all user groups,\n * and users can see user groups to which they belong.\n */\nexport interface ListUserGroups {\n}\n\n/**\n * List permissions for a specific user. **Admin only**.\n * Response: [ListUserTargetPermissionsResponse]\n */\nexport interface ListUserTargetPermissions {\n\t/** Specify either a user or a user group. */\n\tuser_target: UserTarget;\n}\n\n/**\n * **Admin only.**\n * Gets list of Komodo users.\n * Response: [ListUsersResponse]\n */\nexport interface ListUsers {\n}\n\n/**\n * List all available global variables.\n * Response: [ListVariablesResponse]\n * \n * Note. For non admin users making this call,\n * secret variables will have their values obscured.\n */\nexport interface ListVariables {\n}\n\n/**\n * Login as a local user. Will fail if the users credentials don't match\n * any local user.\n * \n * Note. This method is only available if the core api has `local_auth` enabled.\n */\nexport interface LoginLocalUser {\n\t/** The user's username */\n\tusername: string;\n\t/** The user's password */\n\tpassword: string;\n}\n\nexport interface NameAndId {\n\tname: string;\n\tid: string;\n}\n\n/** Configuration for a Ntfy alerter. */\nexport interface NtfyAlerterEndpoint {\n\t/** The ntfy topic URL */\n\turl: string;\n\t/**\n\t * Optional E-Mail Address to enable ntfy email notifications.\n\t * SMTP must be configured on the ntfy server.\n\t */\n\temail?: string;\n}\n\n/** Pauses all containers on the target server. Response: [Update] */\nexport interface PauseAllContainers {\n\t/** Name or id */\n\tserver: string;\n}\n\n/**\n * Pauses the container on the target server. Response: [Update]\n * \n * 1. Runs `docker pause ${container_name}`.\n */\nexport interface PauseContainer {\n\t/** Name or id */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n}\n\n/**\n * Pauses the container for the target deployment. Response: [Update]\n * \n * 1. Runs `docker pause ${container_name}`.\n */\nexport interface PauseDeployment {\n\t/** Name or id */\n\tdeployment: string;\n}\n\n/** Pauses the target stack. `docker compose pause`. Response: [Update] */\nexport interface PauseStack {\n\t/** Id or name */\n\tstack: string;\n\t/**\n\t * Filter to only pause specific services.\n\t * If empty, will pause all services.\n\t */\n\tservices?: string[];\n}\n\nexport interface PermissionToml {\n\t/**\n\t * Id can be:\n\t * - resource name. `id = \"abcd-build\"`\n\t * - regex matching resource names. `id = \"\\^(.+)-build-([0-9]+)$\\\"`\n\t */\n\ttarget: ResourceTarget;\n\t/**\n\t * The permission level:\n\t * - None\n\t * - Read\n\t * - Execute\n\t * - Write\n\t */\n\tlevel?: PermissionLevel;\n\t/** Any [SpecificPermissions](SpecificPermission) on the resource */\n\tspecific?: Array<SpecificPermission>;\n}\n\n/**\n * Prunes the docker buildx cache on the target server. Response: [Update].\n * \n * 1. Runs `docker buildx prune -a -f`.\n */\nexport interface PruneBuildx {\n\t/** Id or name */\n\tserver: string;\n}\n\n/**\n * Prunes the docker containers on the target server. Response: [Update].\n * \n * 1. Runs `docker container prune -f`.\n */\nexport interface PruneContainers {\n\t/** Id or name */\n\tserver: string;\n}\n\n/**\n * Prunes the docker builders (build cache) on the target server. Response: [Update].\n * \n * 1. Runs `docker builder prune -a -f`.\n */\nexport interface PruneDockerBuilders {\n\t/** Id or name */\n\tserver: string;\n}\n\n/**\n * Prunes the docker images on the target server. Response: [Update].\n * \n * 1. Runs `docker image prune -a -f`.\n */\nexport interface PruneImages {\n\t/** Id or name */\n\tserver: string;\n}\n\n/**\n * Prunes the docker networks on the target server. Response: [Update].\n * \n * 1. Runs `docker network prune -f`.\n */\nexport interface PruneNetworks {\n\t/** Id or name */\n\tserver: string;\n}\n\n/**\n * Prunes the docker system on the target server, including volumes. Response: [Update].\n * \n * 1. Runs `docker system prune -a -f --volumes`.\n */\nexport interface PruneSystem {\n\t/** Id or name */\n\tserver: string;\n}\n\n/**\n * Prunes the docker volumes on the target server. Response: [Update].\n * \n * 1. Runs `docker volume prune -a -f`.\n */\nexport interface PruneVolumes {\n\t/** Id or name */\n\tserver: string;\n}\n\n/** Pulls the image for the target deployment. Response: [Update] */\nexport interface PullDeployment {\n\t/** Name or id */\n\tdeployment: string;\n}\n\n/**\n * Pulls the target repo. Response: [Update].\n * \n * Note. Repo must have server attached at `server_id`.\n * \n * 1. Pulls the repo on the target server using `git pull`.\n * 2. If `on_pull` is specified, it will be executed after the pull is complete.\n */\nexport interface PullRepo {\n\t/** Id or name */\n\trepo: string;\n}\n\n/** Pulls images for the target stack. `docker compose pull`. Response: [Update] */\nexport interface PullStack {\n\t/** Id or name */\n\tstack: string;\n\t/**\n\t * Filter to only pull specific services.\n\t * If empty, will pull all services.\n\t */\n\tservices?: string[];\n}\n\n/**\n * Push a resource to the front of the users 10 most recently viewed resources.\n * Response: [NoData].\n */\nexport interface PushRecentlyViewed {\n\t/** The target to push. */\n\tresource: ResourceTarget;\n}\n\n/** Configuration for a Pushover alerter. */\nexport interface PushoverAlerterEndpoint {\n\t/** The pushover URL including application and user tokens in parameters. */\n\turl: string;\n}\n\n/** Trigger a refresh of the cached latest hash and message. */\nexport interface RefreshBuildCache {\n\t/** Id or name */\n\tbuild: string;\n}\n\n/** Trigger a refresh of the cached latest hash and message. */\nexport interface RefreshRepoCache {\n\t/** Id or name */\n\trepo: string;\n}\n\n/** Trigger a refresh of the computed diff logs for view. Response: [ResourceSync] */\nexport interface RefreshResourceSyncPending {\n\t/** Id or name */\n\tsync: string;\n}\n\n/**\n * Trigger a refresh of the cached compose file contents.\n * Refreshes:\n * - Whether the remote file is missing\n * - The latest json, and for repos, the remote contents, hash, and message.\n */\nexport interface RefreshStackCache {\n\t/** Id or name */\n\tstack: string;\n}\n\n/** **Admin only.** Remove a user from a user group. Response: [UserGroup] */\nexport interface RemoveUserFromUserGroup {\n\t/** The name or id of UserGroup that user should be removed from. */\n\tuser_group: string;\n\t/** The id or username of the user to remove */\n\tuser: string;\n}\n\n/**\n * Rename the Action at id to the given name.\n * Response: [Update].\n */\nexport interface RenameAction {\n\t/** The id or name of the Action to rename. */\n\tid: string;\n\t/** The new name. */\n\tname: string;\n}\n\n/**\n * Rename the Alerter at id to the given name.\n * Response: [Update].\n */\nexport interface RenameAlerter {\n\t/** The id or name of the Alerter to rename. */\n\tid: string;\n\t/** The new name. */\n\tname: string;\n}\n\n/**\n * Rename the Build at id to the given name.\n * Response: [Update].\n */\nexport interface RenameBuild {\n\t/** The id or name of the Build to rename. */\n\tid: string;\n\t/** The new name. */\n\tname: string;\n}\n\n/**\n * Rename the Builder at id to the given name.\n * Response: [Update].\n */\nexport interface RenameBuilder {\n\t/** The id or name of the Builder to rename. */\n\tid: string;\n\t/** The new name. */\n\tname: string;\n}\n\n/**\n * Rename the deployment at id to the given name. Response: [Update].\n * \n * Note. If a container is created for the deployment, it will be renamed using\n * `docker rename ...`.\n */\nexport interface RenameDeployment {\n\t/** The id of the deployment to rename. */\n\tid: string;\n\t/** The new name. */\n\tname: string;\n}\n\n/**\n * Rename the Procedure at id to the given name.\n * Response: [Update].\n */\nexport interface RenameProcedure {\n\t/** The id or name of the Procedure to rename. */\n\tid: string;\n\t/** The new name. */\n\tname: string;\n}\n\n/**\n * Rename the Repo at id to the given name.\n * Response: [Update].\n */\nexport interface RenameRepo {\n\t/** The id or name of the Repo to rename. */\n\tid: string;\n\t/** The new name. */\n\tname: string;\n}\n\n/**\n * Rename the ResourceSync at id to the given name.\n * Response: [Update].\n */\nexport interface RenameResourceSync {\n\t/** The id or name of the ResourceSync to rename. */\n\tid: string;\n\t/** The new name. */\n\tname: string;\n}\n\n/**\n * Rename an Server to the given name.\n * Response: [Update].\n */\nexport interface RenameServer {\n\t/** The id or name of the Server to rename. */\n\tid: string;\n\t/** The new name. */\n\tname: string;\n}\n\n/** Rename the stack at id to the given name. Response: [Update]. */\nexport interface RenameStack {\n\t/** The id of the stack to rename. */\n\tid: string;\n\t/** The new name. */\n\tname: string;\n}\n\n/** Rename a tag at id. Response: [Tag]. */\nexport interface RenameTag {\n\t/** The id of the tag to rename. */\n\tid: string;\n\t/** The new name of the tag. */\n\tname: string;\n}\n\n/** **Admin only.** Rename a user group. Response: [UserGroup] */\nexport interface RenameUserGroup {\n\t/** The id of the UserGroup */\n\tid: string;\n\t/** The new name for the UserGroup */\n\tname: string;\n}\n\nexport enum DefaultRepoFolder {\n\t/** /${root_directory}/stacks */\n\tStacks = \"Stacks\",\n\t/** /${root_directory}/builds */\n\tBuilds = \"Builds\",\n\t/** /${root_directory}/repos */\n\tRepos = \"Repos\",\n\t/**\n\t * If the repo is only cloned\n\t * in the core repo cache (resource sync),\n\t * this isn't relevant.\n\t */\n\tNotApplicable = \"NotApplicable\",\n}\n\nexport interface RepoExecutionArgs {\n\t/** Resource name (eg Build name, Repo name) */\n\tname: string;\n\t/** Git provider domain. Default: `github.com` */\n\tprovider: string;\n\t/** Use https (vs http). */\n\thttps: boolean;\n\t/** Configure the account used to access repo (if private) */\n\taccount?: string;\n\t/**\n\t * Full repo identifier. {namespace}/{repo_name}\n\t * Its optional to force checking and produce error if not defined.\n\t */\n\trepo?: string;\n\t/** Git Branch. Default: `main` */\n\tbranch: string;\n\t/** Specific commit hash. Optional */\n\tcommit?: string;\n\t/** The clone destination path */\n\tdestination?: string;\n\t/**\n\t * The default folder to use.\n\t * Depends on the resource type.\n\t */\n\tdefault_folder: DefaultRepoFolder;\n}\n\nexport interface RepoExecutionResponse {\n\t/** Response logs */\n\tlogs: Log[];\n\t/** Absolute path to the repo root on the host. */\n\tpath: string;\n\t/** Latest short commit hash, if it could be retrieved */\n\tcommit_hash?: string;\n\t/** Latest commit message, if it could be retrieved */\n\tcommit_message?: string;\n}\n\nexport interface ResourceToml<PartialConfig> {\n\t/** The resource name. Required */\n\tname: string;\n\t/** The resource description. Optional. */\n\tdescription?: string;\n\t/** Mark resource as a template */\n\ttemplate?: boolean;\n\t/** Tag ids or names. Optional */\n\ttags?: string[];\n\t/**\n\t * Optional. Only relevant for deployments / stacks.\n\t * \n\t * Will ensure deployment / stack is running with the latest configuration.\n\t * Deploy actions to achieve this will be included in the sync.\n\t * Default is false.\n\t */\n\tdeploy?: boolean;\n\t/**\n\t * Optional. Only relevant for deployments / stacks using the 'deploy' sync feature.\n\t * \n\t * Specify other deployments / stacks by name as dependencies.\n\t * The sync will ensure the deployment / stack will only be deployed 'after' its dependencies.\n\t */\n\tafter?: string[];\n\t/** Resource specific configuration. */\n\tconfig?: PartialConfig;\n}\n\nexport interface UserGroupToml {\n\t/** User group name */\n\tname: string;\n\t/** Whether all users will implicitly have the permissions in this group. */\n\teveryone?: boolean;\n\t/** Users in the group */\n\tusers?: string[];\n\t/** Give the user group elevated permissions on all resources of a certain type */\n\tall?: Record<ResourceTarget[\"type\"], PermissionLevelAndSpecifics | PermissionLevel>;\n\t/** Permissions given to the group */\n\tpermissions?: PermissionToml[];\n}\n\n/** Specifies resources to sync on Komodo */\nexport interface ResourcesToml {\n\tservers?: ResourceToml<_PartialServerConfig>[];\n\tdeployments?: ResourceToml<_PartialDeploymentConfig>[];\n\tstacks?: ResourceToml<_PartialStackConfig>[];\n\tbuilds?: ResourceToml<_PartialBuildConfig>[];\n\trepos?: ResourceToml<_PartialRepoConfig>[];\n\tprocedures?: ResourceToml<_PartialProcedureConfig>[];\n\tactions?: ResourceToml<_PartialActionConfig>[];\n\talerters?: ResourceToml<_PartialAlerterConfig>[];\n\tbuilders?: ResourceToml<_PartialBuilderConfig>[];\n\tresource_syncs?: ResourceToml<_PartialResourceSyncConfig>[];\n\tuser_groups?: UserGroupToml[];\n\tvariables?: Variable[];\n}\n\n/** Restarts all containers on the target server. Response: [Update] */\nexport interface RestartAllContainers {\n\t/** Name or id */\n\tserver: string;\n}\n\n/**\n * Restarts the container on the target server. Response: [Update]\n * \n * 1. Runs `docker restart ${container_name}`.\n */\nexport interface RestartContainer {\n\t/** Name or id */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n}\n\n/**\n * Restarts the container for the target deployment. Response: [Update]\n * \n * 1. Runs `docker restart ${container_name}`.\n */\nexport interface RestartDeployment {\n\t/** Name or id */\n\tdeployment: string;\n}\n\n/** Restarts the target stack. `docker compose restart`. Response: [Update] */\nexport interface RestartStack {\n\t/** Id or name */\n\tstack: string;\n\t/**\n\t * Filter to only restart specific services.\n\t * If empty, will restart all services.\n\t */\n\tservices?: string[];\n}\n\n/** Runs the target Action. Response: [Update] */\nexport interface RunAction {\n\t/** Id or name */\n\taction: string;\n\t/**\n\t * Custom arguments which are merged on top of the default arguments.\n\t * CLI Format: `\"VAR1=val1&VAR2=val2\"`\n\t * \n\t * Webhook-triggered actions use this to pass WEBHOOK_BRANCH and WEBHOOK_BODY.\n\t */\n\targs?: JsonObject;\n}\n\n/**\n * Runs the target build. Response: [Update].\n * \n * 1. Get a handle to the builder. If using AWS builder, this means starting a builder ec2 instance.\n * \n * 2. Clone the repo on the builder. If an `on_clone` commmand is given, it will be executed.\n * \n * 3. Execute `docker build {...params}`, where params are determined using the builds configuration.\n * \n * 4. If a docker registry is configured, the build will be pushed to the registry.\n * \n * 5. If using AWS builder, destroy the builder ec2 instance.\n * \n * 6. Deploy any Deployments with *Redeploy on Build* enabled.\n */\nexport interface RunBuild {\n\t/** Can be build id or name */\n\tbuild: string;\n}\n\n/** Runs the target Procedure. Response: [Update] */\nexport interface RunProcedure {\n\t/** Id or name */\n\tprocedure: string;\n}\n\n/** Runs a one-time command against a service using `docker compose run`. Response: [Update] */\nexport interface RunStackService {\n\t/** Id or name */\n\tstack: string;\n\t/** Service to run */\n\tservice: string;\n\t/** Command and args to pass to the service container */\n\tcommand?: string[];\n\t/** Do not allocate TTY */\n\tno_tty?: boolean;\n\t/** Do not start linked services */\n\tno_deps?: boolean;\n\t/** Detach container on run */\n\tdetach?: boolean;\n\t/** Map service ports to the host */\n\tservice_ports?: boolean;\n\t/** Extra environment variables for the run */\n\tenv?: Record<string, string>;\n\t/** Working directory inside the container */\n\tworkdir?: string;\n\t/** User to run as inside the container */\n\tuser?: string;\n\t/** Override the default entrypoint */\n\tentrypoint?: string;\n\t/** Pull the image before running */\n\tpull?: boolean;\n}\n\n/** Runs the target resource sync. Response: [Update] */\nexport interface RunSync {\n\t/** Id or name */\n\tsync: string;\n\t/**\n\t * Only execute sync on a specific resource type.\n\t * Combine with `resource_id` to specify resource.\n\t */\n\tresource_type?: ResourceTarget[\"type\"];\n\t/**\n\t * Only execute sync on a specific resources.\n\t * Combine with `resource_type` to specify resources.\n\t * Supports name or id.\n\t */\n\tresources?: string[];\n}\n\nexport enum SearchCombinator {\n\tOr = \"Or\",\n\tAnd = \"And\",\n}\n\n/**\n * Search the container log's tail using `grep`. All lines go to stdout.\n * Response: [Log].\n * \n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface SearchContainerLog {\n\t/** Id or name */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n\t/** The terms to search for. */\n\tterms: string[];\n\t/**\n\t * When searching for multiple terms, can use `AND` or `OR` combinator.\n\t * \n\t * - `AND`: Only include lines with **all** terms present in that line.\n\t * - `OR`: Include lines that have one or more matches in the terms.\n\t */\n\tcombinator?: SearchCombinator;\n\t/** Invert the results, ie return all lines that DON'T match the terms / combinator. */\n\tinvert?: boolean;\n\t/** Enable `--timestamps` */\n\ttimestamps?: boolean;\n}\n\n/**\n * Search the deployment log's tail using `grep`. All lines go to stdout.\n * Response: [Log].\n * \n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface SearchDeploymentLog {\n\t/** Id or name */\n\tdeployment: string;\n\t/** The terms to search for. */\n\tterms: string[];\n\t/**\n\t * When searching for multiple terms, can use `AND` or `OR` combinator.\n\t * \n\t * - `AND`: Only include lines with **all** terms present in that line.\n\t * - `OR`: Include lines that have one or more matches in the terms.\n\t */\n\tcombinator?: SearchCombinator;\n\t/** Invert the results, ie return all lines that DON'T match the terms / combinator. */\n\tinvert?: boolean;\n\t/** Enable `--timestamps` */\n\ttimestamps?: boolean;\n}\n\n/**\n * Search the stack log's tail using `grep`. All lines go to stdout.\n * Response: [SearchStackLogResponse].\n * \n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface SearchStackLog {\n\t/** Id or name */\n\tstack: string;\n\t/**\n\t * Filter the logs to only ones from specific services.\n\t * If empty, will include logs from all services.\n\t */\n\tservices: string[];\n\t/** The terms to search for. */\n\tterms: string[];\n\t/**\n\t * When searching for multiple terms, can use `AND` or `OR` combinator.\n\t * \n\t * - `AND`: Only include lines with **all** terms present in that line.\n\t * - `OR`: Include lines that have one or more matches in the terms.\n\t */\n\tcombinator?: SearchCombinator;\n\t/** Invert the results, ie return all lines that DON'T match the terms / combinator. */\n\tinvert?: boolean;\n\t/** Enable `--timestamps` */\n\ttimestamps?: boolean;\n}\n\n/** Send a custom alert message to configured Alerters. Response: [Update] */\nexport interface SendAlert {\n\t/** The alert level. */\n\tlevel?: SeverityLevel;\n\t/** The alert message. Required. */\n\tmessage: string;\n\t/** The alert details. Optional. */\n\tdetails?: string;\n\t/**\n\t * Specific alerter names or ids.\n\t * If empty / not passed, sends to all configured alerters\n\t * with the `Custom` alert type whitelisted / not blacklisted.\n\t */\n\talerters?: string[];\n}\n\n/** Configuration for a Komodo Server Builder. */\nexport interface ServerBuilderConfig {\n\t/** The server id of the builder */\n\tserver_id?: string;\n}\n\n/** The health of a part of the server. */\nexport interface ServerHealthState {\n\tlevel: SeverityLevel;\n\t/** Whether the health is good enough to close an open alert. */\n\tshould_close_alert: boolean;\n}\n\n/** Summary of the health of the server. */\nexport interface ServerHealth {\n\tcpu: ServerHealthState;\n\tmem: ServerHealthState;\n\tdisks: Record<string, ServerHealthState>;\n}\n\n/**\n * **Admin only.** Set `everyone` property of User Group.\n * Response: [UserGroup]\n */\nexport interface SetEveryoneUserGroup {\n\t/** Id or name. */\n\tuser_group: string;\n\t/** Whether this user group applies to everyone. */\n\teveryone: boolean;\n}\n\n/**\n * Set the time the user last opened the UI updates.\n * Used for unseen notification dot.\n * Response: [NoData]\n */\nexport interface SetLastSeenUpdate {\n}\n\n/**\n * **Admin only.** Completely override the users in the group.\n * Response: [UserGroup]\n */\nexport interface SetUsersInUserGroup {\n\t/** Id or name. */\n\tuser_group: string;\n\t/** The user ids or usernames to hard set as the group's users. */\n\tusers: string[];\n}\n\n/**\n * Sign up a new local user account. Will fail if a user with the\n * given username already exists.\n * Response: [SignUpLocalUserResponse].\n * \n * Note. This method is only available if the core api has `local_auth` enabled,\n * and if user registration is not disabled (after the first user).\n */\nexport interface SignUpLocalUser {\n\t/** The username for the new user. */\n\tusername: string;\n\t/**\n\t * The password for the new user.\n\t * This cannot be retreived later.\n\t */\n\tpassword: string;\n}\n\n/** Info for network interface usage. */\nexport interface SingleNetworkInterfaceUsage {\n\t/** The network interface name */\n\tname: string;\n\t/** The ingress in bytes */\n\tingress_bytes: number;\n\t/** The egress in bytes */\n\tegress_bytes: number;\n}\n\n/** Configuration for a Slack alerter. */\nexport interface SlackAlerterEndpoint {\n\t/** The Slack app webhook url */\n\turl: string;\n}\n\n/** Sleeps for the specified time. */\nexport interface Sleep {\n\tduration_ms?: I64;\n}\n\n/** Starts all containers on the target server. Response: [Update] */\nexport interface StartAllContainers {\n\t/** Name or id */\n\tserver: string;\n}\n\n/**\n * Starts the container on the target server. Response: [Update]\n * \n * 1. Runs `docker start ${container_name}`.\n */\nexport interface StartContainer {\n\t/** Name or id */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n}\n\n/**\n * Starts the container for the target deployment. Response: [Update]\n * \n * 1. Runs `docker start ${container_name}`.\n */\nexport interface StartDeployment {\n\t/** Name or id */\n\tdeployment: string;\n}\n\n/** Starts the target stack. `docker compose start`. Response: [Update] */\nexport interface StartStack {\n\t/** Id or name */\n\tstack: string;\n\t/**\n\t * Filter to only start specific services.\n\t * If empty, will start all services.\n\t */\n\tservices?: string[];\n}\n\n/** Stops all containers on the target server. Response: [Update] */\nexport interface StopAllContainers {\n\t/** Name or id */\n\tserver: string;\n}\n\n/**\n * Stops the container on the target server. Response: [Update]\n * \n * 1. Runs `docker stop ${container_name}`.\n */\nexport interface StopContainer {\n\t/** Name or id */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n\t/** Override the default termination signal. */\n\tsignal?: TerminationSignal;\n\t/** Override the default termination max time. */\n\ttime?: number;\n}\n\n/**\n * Stops the container for the target deployment. Response: [Update]\n * \n * 1. Runs `docker stop ${container_name}`.\n */\nexport interface StopDeployment {\n\t/** Name or id */\n\tdeployment: string;\n\t/** Override the default termination signal specified in the deployment. */\n\tsignal?: TerminationSignal;\n\t/** Override the default termination max time. */\n\ttime?: number;\n}\n\n/** Stops the target stack. `docker compose stop`. Response: [Update] */\nexport interface StopStack {\n\t/** Id or name */\n\tstack: string;\n\t/** Override the default termination max time. */\n\tstop_time?: number;\n\t/**\n\t * Filter to only stop specific services.\n\t * If empty, will stop all services.\n\t */\n\tservices?: string[];\n}\n\nexport interface TerminationSignalLabel {\n\tsignal: TerminationSignal;\n\tlabel: string;\n}\n\n/** Tests an Alerters ability to reach the configured endpoint. Response: [Update] */\nexport interface TestAlerter {\n\t/** Name or id */\n\talerter: string;\n}\n\n/** Info for the all system disks combined. */\nexport interface TotalDiskUsage {\n\t/** Used portion in GB */\n\tused_gb: number;\n\t/** Total size in GB */\n\ttotal_gb: number;\n}\n\n/** Unpauses all containers on the target server. Response: [Update] */\nexport interface UnpauseAllContainers {\n\t/** Name or id */\n\tserver: string;\n}\n\n/**\n * Unpauses the container on the target server. Response: [Update]\n * \n * 1. Runs `docker unpause ${container_name}`.\n * \n * Note. This is the only way to restart a paused container.\n */\nexport interface UnpauseContainer {\n\t/** Name or id */\n\tserver: string;\n\t/** The container name */\n\tcontainer: string;\n}\n\n/**\n * Unpauses the container for the target deployment. Response: [Update]\n * \n * 1. Runs `docker unpause ${container_name}`.\n * \n * Note. This is the only way to restart a paused container.\n */\nexport interface UnpauseDeployment {\n\t/** Name or id */\n\tdeployment: string;\n}\n\n/**\n * Unpauses the target stack. `docker compose unpause`. Response: [Update].\n * \n * Note. This is the only way to restart a paused container.\n */\nexport interface UnpauseStack {\n\t/** Id or name */\n\tstack: string;\n\t/**\n\t * Filter to only unpause specific services.\n\t * If empty, will unpause all services.\n\t */\n\tservices?: string[];\n}\n\n/**\n * Update the action at the given id, and return the updated action.\n * Response: [Action].\n * \n * Note. This method updates only the fields which are set in the [_PartialActionConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateAction {\n\t/** The id of the action to update. */\n\tid: string;\n\t/** The partial config update to apply. */\n\tconfig: _PartialActionConfig;\n}\n\n/**\n * Update the alerter at the given id, and return the updated alerter. Response: [Alerter].\n * \n * Note. This method updates only the fields which are set in the [PartialAlerterConfig][crate::entities::alerter::PartialAlerterConfig],\n * effectively merging diffs into the final document. This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateAlerter {\n\t/** The id of the alerter to update. */\n\tid: string;\n\t/** The partial config update to apply. */\n\tconfig: _PartialAlerterConfig;\n}\n\n/**\n * Update the build at the given id, and return the updated build.\n * Response: [Build].\n * \n * Note. This method updates only the fields which are set in the [_PartialBuildConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateBuild {\n\t/** The id or name of the build to update. */\n\tid: string;\n\t/** The partial config update to apply. */\n\tconfig: _PartialBuildConfig;\n}\n\n/**\n * Update the builder at the given id, and return the updated builder.\n * Response: [Builder].\n * \n * Note. This method updates only the fields which are set in the [PartialBuilderConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateBuilder {\n\t/** The id of the builder to update. */\n\tid: string;\n\t/** The partial config update to apply. */\n\tconfig: PartialBuilderConfig;\n}\n\n/**\n * Update the deployment at the given id, and return the updated deployment.\n * Response: [Deployment].\n * \n * Note. If the attached server for the deployment changes,\n * the deployment will be deleted / cleaned up on the old server.\n * \n * Note. This method updates only the fields which are set in the [_PartialDeploymentConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateDeployment {\n\t/** The deployment id to update. */\n\tid: string;\n\t/** The partial config update. */\n\tconfig: _PartialDeploymentConfig;\n}\n\n/**\n * **Admin only.** Update a docker registry account.\n * Response: [DockerRegistryAccount].\n */\nexport interface UpdateDockerRegistryAccount {\n\t/** The id of the docker registry to update */\n\tid: string;\n\t/** The partial docker registry account. */\n\taccount: _PartialDockerRegistryAccount;\n}\n\n/**\n * **Admin only.** Update a git provider account.\n * Response: [GitProviderAccount].\n */\nexport interface UpdateGitProviderAccount {\n\t/** The id of the git provider account to update. */\n\tid: string;\n\t/** The partial git provider account. */\n\taccount: _PartialGitProviderAccount;\n}\n\n/**\n * **Admin only.** Update a user or user groups base permission level on a resource type.\n * Response: [NoData].\n */\nexport interface UpdatePermissionOnResourceType {\n\t/** Specify the user or user group. */\n\tuser_target: UserTarget;\n\t/** The resource type: eg. Server, Build, Deployment, etc. */\n\tresource_type: ResourceTarget[\"type\"];\n\t/** The base permission level. */\n\tpermission: PermissionLevelAndSpecifics | PermissionLevel;\n}\n\n/**\n * **Admin only.** Update a user or user groups permission on a resource.\n * Response: [NoData].\n */\nexport interface UpdatePermissionOnTarget {\n\t/** Specify the user or user group. */\n\tuser_target: UserTarget;\n\t/** Specify the target resource. */\n\tresource_target: ResourceTarget;\n\t/** Specify the permission level. */\n\tpermission: PermissionLevelAndSpecifics | PermissionLevel;\n}\n\n/**\n * Update the procedure at the given id, and return the updated procedure.\n * Response: [Procedure].\n * \n * Note. This method updates only the fields which are set in the [_PartialProcedureConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateProcedure {\n\t/** The id of the procedure to update. */\n\tid: string;\n\t/** The partial config update. */\n\tconfig: _PartialProcedureConfig;\n}\n\n/**\n * Update the repo at the given id, and return the updated repo.\n * Response: [Repo].\n * \n * Note. If the attached server for the repo changes,\n * the repo will be deleted / cleaned up on the old server.\n * \n * Note. This method updates only the fields which are set in the [_PartialRepoConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateRepo {\n\t/** The id of the repo to update. */\n\tid: string;\n\t/** The partial config update to apply. */\n\tconfig: _PartialRepoConfig;\n}\n\n/**\n * Update a resources common meta fields.\n * - description\n * - template\n * - tags\n * Response: [NoData].\n */\nexport interface UpdateResourceMeta {\n\t/** The target resource to set update meta. */\n\ttarget: ResourceTarget;\n\t/**\n\t * New description to set,\n\t * or null for no update\n\t */\n\tdescription?: string;\n\t/**\n\t * New template value (true or false),\n\t * or null for no update\n\t */\n\ttemplate?: boolean;\n\t/**\n\t * The exact tags to set,\n\t * or null for no update\n\t */\n\ttags?: string[];\n}\n\n/**\n * Update the sync at the given id, and return the updated sync.\n * Response: [ResourceSync].\n * \n * Note. This method updates only the fields which are set in the [_PartialResourceSyncConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateResourceSync {\n\t/** The id of the sync to update. */\n\tid: string;\n\t/** The partial config update to apply. */\n\tconfig: _PartialResourceSyncConfig;\n}\n\n/**\n * Update the server at the given id, and return the updated server.\n * Response: [Server].\n * \n * Note. This method updates only the fields which are set in the [_PartialServerConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateServer {\n\t/** The id or name of the server to update. */\n\tid: string;\n\t/** The partial config update to apply. */\n\tconfig: _PartialServerConfig;\n}\n\n/**\n * **Admin only.** Update a service user's description.\n * Response: [User].\n */\nexport interface UpdateServiceUserDescription {\n\t/** The service user's username */\n\tusername: string;\n\t/** A new description for the service user. */\n\tdescription: string;\n}\n\n/**\n * Update the stack at the given id, and return the updated stack.\n * Response: [Stack].\n * \n * Note. If the attached server for the stack changes,\n * the stack will be deleted / cleaned up on the old server.\n * \n * Note. This method updates only the fields which are set in the [_PartialStackConfig],\n * merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateStack {\n\t/** The id of the Stack to update. */\n\tid: string;\n\t/** The partial config update to apply. */\n\tconfig: _PartialStackConfig;\n}\n\n/** Update color for tag. Response: [Tag]. */\nexport interface UpdateTagColor {\n\t/** The name or id of the tag to update. */\n\ttag: string;\n\t/** The new color for the tag. */\n\tcolor: TagColor;\n}\n\n/**\n * **Super Admin only.** Update's whether a user is admin.\n * Response: [NoData].\n */\nexport interface UpdateUserAdmin {\n\t/** The target user. */\n\tuser_id: string;\n\t/** Whether user should be admin. */\n\tadmin: boolean;\n}\n\n/**\n * **Admin only.** Update a user's \"base\" permissions, eg. \"enabled\".\n * Response: [NoData].\n */\nexport interface UpdateUserBasePermissions {\n\t/** The target user. */\n\tuser_id: string;\n\t/** If specified, will update users enabled state. */\n\tenabled?: boolean;\n\t/** If specified, will update user's ability to create servers. */\n\tcreate_servers?: boolean;\n\t/** If specified, will update user's ability to create builds. */\n\tcreate_builds?: boolean;\n}\n\n/**\n * **Only for local users**. Update the calling users password.\n * Response: [NoData].\n */\nexport interface UpdateUserPassword {\n\tpassword: string;\n}\n\n/**\n * **Only for local users**. Update the calling users username.\n * Response: [NoData].\n */\nexport interface UpdateUserUsername {\n\tusername: string;\n}\n\n/** **Admin only.** Update variable description. Response: [Variable]. */\nexport interface UpdateVariableDescription {\n\t/** The name of the variable to update. */\n\tname: string;\n\t/** The description to set. */\n\tdescription: string;\n}\n\n/** **Admin only.** Update whether variable is secret. Response: [Variable]. */\nexport interface UpdateVariableIsSecret {\n\t/** The name of the variable to update. */\n\tname: string;\n\t/** Whether variable is secret. */\n\tis_secret: boolean;\n}\n\n/** **Admin only.** Update variable value. Response: [Variable]. */\nexport interface UpdateVariableValue {\n\t/** The name of the variable to update. */\n\tname: string;\n\t/** The value to set. */\n\tvalue: string;\n}\n\n/** Configuration for a Komodo Url Builder. */\nexport interface UrlBuilderConfig {\n\t/** The address of the Periphery agent */\n\taddress: string;\n\t/** A custom passkey to use. Otherwise, use the default passkey. */\n\tpasskey?: string;\n}\n\n/** Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update]. */\nexport interface WriteBuildFileContents {\n\t/** The name or id of the target Build. */\n\tbuild: string;\n\t/** The dockerfile contents to write. */\n\tcontents: string;\n}\n\n/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */\nexport interface WriteStackFileContents {\n\t/** The name or id of the target Stack. */\n\tstack: string;\n\t/**\n\t * The file path relative to the stack run directory,\n\t * or absolute path.\n\t */\n\tfile_path: string;\n\t/** The contents to write. */\n\tcontents: string;\n}\n\n/** Rename the stack at id to the given name. Response: [Update]. */\nexport interface WriteSyncFileContents {\n\t/** The name or id of the target Sync. */\n\tsync: string;\n\t/**\n\t * If this file was under a resource folder, this will be the folder.\n\t * Otherwise, it should be empty string.\n\t */\n\tresource_path: string;\n\t/** The file path relative to the resource path. */\n\tfile_path: string;\n\t/** The contents to write. */\n\tcontents: string;\n}\n\nexport type AuthRequest = \n\t| { type: \"GetLoginOptions\", params: GetLoginOptions }\n\t| { type: \"SignUpLocalUser\", params: SignUpLocalUser }\n\t| { type: \"LoginLocalUser\", params: LoginLocalUser }\n\t| { type: \"ExchangeForJwt\", params: ExchangeForJwt }\n\t| { type: \"GetUser\", params: GetUser };\n\n/** Days of the week */\nexport enum DayOfWeek {\n\tMonday = \"Monday\",\n\tTuesday = \"Tuesday\",\n\tWednesday = \"Wednesday\",\n\tThursday = \"Thursday\",\n\tFriday = \"Friday\",\n\tSaturday = \"Saturday\",\n\tSunday = \"Sunday\",\n}\n\nexport type ExecuteRequest = \n\t| { type: \"StartContainer\", params: StartContainer }\n\t| { type: \"RestartContainer\", params: RestartContainer }\n\t| { type: \"PauseContainer\", params: PauseContainer }\n\t| { type: \"UnpauseContainer\", params: UnpauseContainer }\n\t| { type: \"StopContainer\", params: StopContainer }\n\t| { type: \"DestroyContainer\", params: DestroyContainer }\n\t| { type: \"StartAllContainers\", params: StartAllContainers }\n\t| { type: \"RestartAllContainers\", params: RestartAllContainers }\n\t| { type: \"PauseAllContainers\", params: PauseAllContainers }\n\t| { type: \"UnpauseAllContainers\", params: UnpauseAllContainers }\n\t| { type: \"StopAllContainers\", params: StopAllContainers }\n\t| { type: \"PruneContainers\", params: PruneContainers }\n\t| { type: \"DeleteNetwork\", params: DeleteNetwork }\n\t| { type: \"PruneNetworks\", params: PruneNetworks }\n\t| { type: \"DeleteImage\", params: DeleteImage }\n\t| { type: \"PruneImages\", params: PruneImages }\n\t| { type: \"DeleteVolume\", params: DeleteVolume }\n\t| { type: \"PruneVolumes\", params: PruneVolumes }\n\t| { type: \"PruneDockerBuilders\", params: PruneDockerBuilders }\n\t| { type: \"PruneBuildx\", params: PruneBuildx }\n\t| { type: \"PruneSystem\", params: PruneSystem }\n\t| { type: \"DeployStack\", params: DeployStack }\n\t| { type: \"BatchDeployStack\", params: BatchDeployStack }\n\t| { type: \"DeployStackIfChanged\", params: DeployStackIfChanged }\n\t| { type: \"BatchDeployStackIfChanged\", params: BatchDeployStackIfChanged }\n\t| { type: \"PullStack\", params: PullStack }\n\t| { type: \"BatchPullStack\", params: BatchPullStack }\n\t| { type: \"StartStack\", params: StartStack }\n\t| { type: \"RestartStack\", params: RestartStack }\n\t| { type: \"StopStack\", params: StopStack }\n\t| { type: \"PauseStack\", params: PauseStack }\n\t| { type: \"UnpauseStack\", params: UnpauseStack }\n\t| { type: \"DestroyStack\", params: DestroyStack }\n\t| { type: \"BatchDestroyStack\", params: BatchDestroyStack }\n\t| { type: \"RunStackService\", params: RunStackService }\n\t| { type: \"Deploy\", params: Deploy }\n\t| { type: \"BatchDeploy\", params: BatchDeploy }\n\t| { type: \"PullDeployment\", params: PullDeployment }\n\t| { type: \"StartDeployment\", params: StartDeployment }\n\t| { type: \"RestartDeployment\", params: RestartDeployment }\n\t| { type: \"PauseDeployment\", params: PauseDeployment }\n\t| { type: \"UnpauseDeployment\", params: UnpauseDeployment }\n\t| { type: \"StopDeployment\", params: StopDeployment }\n\t| { type: \"DestroyDeployment\", params: DestroyDeployment }\n\t| { type: \"BatchDestroyDeployment\", params: BatchDestroyDeployment }\n\t| { type: \"RunBuild\", params: RunBuild }\n\t| { type: \"BatchRunBuild\", params: BatchRunBuild }\n\t| { type: \"CancelBuild\", params: CancelBuild }\n\t| { type: \"CloneRepo\", params: CloneRepo }\n\t| { type: \"BatchCloneRepo\", params: BatchCloneRepo }\n\t| { type: \"PullRepo\", params: PullRepo }\n\t| { type: \"BatchPullRepo\", params: BatchPullRepo }\n\t| { type: \"BuildRepo\", params: BuildRepo }\n\t| { type: \"BatchBuildRepo\", params: BatchBuildRepo }\n\t| { type: \"CancelRepoBuild\", params: CancelRepoBuild }\n\t| { type: \"RunProcedure\", params: RunProcedure }\n\t| { type: \"BatchRunProcedure\", params: BatchRunProcedure }\n\t| { type: \"RunAction\", params: RunAction }\n\t| { type: \"BatchRunAction\", params: BatchRunAction }\n\t| { type: \"TestAlerter\", params: TestAlerter }\n\t| { type: \"SendAlert\", params: SendAlert }\n\t| { type: \"RunSync\", params: RunSync }\n\t| { type: \"ClearRepoCache\", params: ClearRepoCache }\n\t| { type: \"BackupCoreDatabase\", params: BackupCoreDatabase }\n\t| { type: \"GlobalAutoUpdate\", params: GlobalAutoUpdate };\n\n/**\n * One representative IANA zone for each distinct base UTC offset in the tz database.\n * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n * \n * The `serde`/`strum` renames ensure the canonical identifier is used\n * when serializing or parsing from a string such as `\"Etc/UTC\"`.\n */\nexport enum IanaTimezone {\n\t/** UTC−12:00 */\n\tEtcGmtMinus12 = \"Etc/GMT+12\",\n\t/** UTC−11:00 */\n\tPacificPagoPago = \"Pacific/Pago_Pago\",\n\t/** UTC−10:00 */\n\tPacificHonolulu = \"Pacific/Honolulu\",\n\t/** UTC−09:30 */\n\tPacificMarquesas = \"Pacific/Marquesas\",\n\t/** UTC−09:00 */\n\tAmericaAnchorage = \"America/Anchorage\",\n\t/** UTC−08:00 */\n\tAmericaLosAngeles = \"America/Los_Angeles\",\n\t/** UTC−07:00 */\n\tAmericaDenver = \"America/Denver\",\n\t/** UTC−06:00 */\n\tAmericaChicago = \"America/Chicago\",\n\t/** UTC−05:00 */\n\tAmericaNewYork = \"America/New_York\",\n\t/** UTC−04:00 */\n\tAmericaHalifax = \"America/Halifax\",\n\t/** UTC−03:30 */\n\tAmericaStJohns = \"America/St_Johns\",\n\t/** UTC−03:00 */\n\tAmericaSaoPaulo = \"America/Sao_Paulo\",\n\t/** UTC−02:00 */\n\tAmericaNoronha = \"America/Noronha\",\n\t/** UTC−01:00 */\n\tAtlanticAzores = \"Atlantic/Azores\",\n\t/** UTC±00:00 */\n\tEtcUtc = \"Etc/UTC\",\n\t/** UTC+01:00 */\n\tEuropeBerlin = \"Europe/Berlin\",\n\t/** UTC+02:00 */\n\tEuropeBucharest = \"Europe/Bucharest\",\n\t/** UTC+03:00 */\n\tEuropeMoscow = \"Europe/Moscow\",\n\t/** UTC+03:30 */\n\tAsiaTehran = \"Asia/Tehran\",\n\t/** UTC+04:00 */\n\tAsiaDubai = \"Asia/Dubai\",\n\t/** UTC+04:30 */\n\tAsiaKabul = \"Asia/Kabul\",\n\t/** UTC+05:00 */\n\tAsiaKarachi = \"Asia/Karachi\",\n\t/** UTC+05:30 */\n\tAsiaKolkata = \"Asia/Kolkata\",\n\t/** UTC+05:45 */\n\tAsiaKathmandu = \"Asia/Kathmandu\",\n\t/** UTC+06:00 */\n\tAsiaDhaka = \"Asia/Dhaka\",\n\t/** UTC+06:30 */\n\tAsiaYangon = \"Asia/Yangon\",\n\t/** UTC+07:00 */\n\tAsiaBangkok = \"Asia/Bangkok\",\n\t/** UTC+08:00 */\n\tAsiaShanghai = \"Asia/Shanghai\",\n\t/** UTC+08:45 */\n\tAustraliaEucla = \"Australia/Eucla\",\n\t/** UTC+09:00 */\n\tAsiaTokyo = \"Asia/Tokyo\",\n\t/** UTC+09:30 */\n\tAustraliaAdelaide = \"Australia/Adelaide\",\n\t/** UTC+10:00 */\n\tAustraliaSydney = \"Australia/Sydney\",\n\t/** UTC+10:30 */\n\tAustraliaLordHowe = \"Australia/Lord_Howe\",\n\t/** UTC+11:00 */\n\tPacificPortMoresby = \"Pacific/Port_Moresby\",\n\t/** UTC+12:00 */\n\tPacificAuckland = \"Pacific/Auckland\",\n\t/** UTC+12:45 */\n\tPacificChatham = \"Pacific/Chatham\",\n\t/** UTC+13:00 */\n\tPacificTongatapu = \"Pacific/Tongatapu\",\n\t/** UTC+14:00 */\n\tPacificKiritimati = \"Pacific/Kiritimati\",\n}\n\nexport type ReadRequest = \n\t| { type: \"GetVersion\", params: GetVersion }\n\t| { type: \"GetCoreInfo\", params: GetCoreInfo }\n\t| { type: \"ListSecrets\", params: ListSecrets }\n\t| { type: \"ListGitProvidersFromConfig\", params: ListGitProvidersFromConfig }\n\t| { type: \"ListDockerRegistriesFromConfig\", params: ListDockerRegistriesFromConfig }\n\t| { type: \"GetUsername\", params: GetUsername }\n\t| { type: \"GetPermission\", params: GetPermission }\n\t| { type: \"FindUser\", params: FindUser }\n\t| { type: \"ListUsers\", params: ListUsers }\n\t| { type: \"ListApiKeys\", params: ListApiKeys }\n\t| { type: \"ListApiKeysForServiceUser\", params: ListApiKeysForServiceUser }\n\t| { type: \"ListPermissions\", params: ListPermissions }\n\t| { type: \"ListUserTargetPermissions\", params: ListUserTargetPermissions }\n\t| { type: \"GetUserGroup\", params: GetUserGroup }\n\t| { type: \"ListUserGroups\", params: ListUserGroups }\n\t| { type: \"GetProceduresSummary\", params: GetProceduresSummary }\n\t| { type: \"GetProcedure\", params: GetProcedure }\n\t| { type: \"GetProcedureActionState\", params: GetProcedureActionState }\n\t| { type: \"ListProcedures\", params: ListProcedures }\n\t| { type: \"ListFullProcedures\", params: ListFullProcedures }\n\t| { type: \"GetActionsSummary\", params: GetActionsSummary }\n\t| { type: \"GetAction\", params: GetAction }\n\t| { type: \"GetActionActionState\", params: GetActionActionState }\n\t| { type: \"ListActions\", params: ListActions }\n\t| { type: \"ListFullActions\", params: ListFullActions }\n\t| { type: \"ListSchedules\", params: ListSchedules }\n\t| { type: \"GetServersSummary\", params: GetServersSummary }\n\t| { type: \"GetServer\", params: GetServer }\n\t| { type: \"GetServerState\", params: GetServerState }\n\t| { type: \"GetPeripheryVersion\", params: GetPeripheryVersion }\n\t| { type: \"GetServerActionState\", params: GetServerActionState }\n\t| { type: \"GetHistoricalServerStats\", params: GetHistoricalServerStats }\n\t| { type: \"ListServers\", params: ListServers }\n\t| { type: \"ListFullServers\", params: ListFullServers }\n\t| { type: \"InspectDockerContainer\", params: InspectDockerContainer }\n\t| { type: \"GetResourceMatchingContainer\", params: GetResourceMatchingContainer }\n\t| { type: \"GetContainerLog\", params: GetContainerLog }\n\t| { type: \"SearchContainerLog\", params: SearchContainerLog }\n\t| { type: \"InspectDockerNetwork\", params: InspectDockerNetwork }\n\t| { type: \"InspectDockerImage\", params: InspectDockerImage }\n\t| { type: \"ListDockerImageHistory\", params: ListDockerImageHistory }\n\t| { type: \"InspectDockerVolume\", params: InspectDockerVolume }\n\t| { type: \"GetDockerContainersSummary\", params: GetDockerContainersSummary }\n\t| { type: \"ListAllDockerContainers\", params: ListAllDockerContainers }\n\t| { type: \"ListDockerContainers\", params: ListDockerContainers }\n\t| { type: \"ListDockerNetworks\", params: ListDockerNetworks }\n\t| { type: \"ListDockerImages\", params: ListDockerImages }\n\t| { type: \"ListDockerVolumes\", params: ListDockerVolumes }\n\t| { type: \"ListComposeProjects\", params: ListComposeProjects }\n\t| { type: \"ListTerminals\", params: ListTerminals }\n\t| { type: \"GetSystemInformation\", params: GetSystemInformation }\n\t| { type: \"GetSystemStats\", params: GetSystemStats }\n\t| { type: \"ListSystemProcesses\", params: ListSystemProcesses }\n\t| { type: \"GetStacksSummary\", params: GetStacksSummary }\n\t| { type: \"GetStack\", params: GetStack }\n\t| { type: \"GetStackActionState\", params: GetStackActionState }\n\t| { type: \"GetStackWebhooksEnabled\", params: GetStackWebhooksEnabled }\n\t| { type: \"GetStackLog\", params: GetStackLog }\n\t| { type: \"SearchStackLog\", params: SearchStackLog }\n\t| { type: \"InspectStackContainer\", params: InspectStackContainer }\n\t| { type: \"ListStacks\", params: ListStacks }\n\t| { type: \"ListFullStacks\", params: ListFullStacks }\n\t| { type: \"ListStackServices\", params: ListStackServices }\n\t| { type: \"ListCommonStackExtraArgs\", params: ListCommonStackExtraArgs }\n\t| { type: \"ListCommonStackBuildExtraArgs\", params: ListCommonStackBuildExtraArgs }\n\t| { type: \"GetDeploymentsSummary\", params: GetDeploymentsSummary }\n\t| { type: \"GetDeployment\", params: GetDeployment }\n\t| { type: \"GetDeploymentContainer\", params: GetDeploymentContainer }\n\t| { type: \"GetDeploymentActionState\", params: GetDeploymentActionState }\n\t| { type: \"GetDeploymentStats\", params: GetDeploymentStats }\n\t| { type: \"GetDeploymentLog\", params: GetDeploymentLog }\n\t| { type: \"SearchDeploymentLog\", params: SearchDeploymentLog }\n\t| { type: \"InspectDeploymentContainer\", params: InspectDeploymentContainer }\n\t| { type: \"ListDeployments\", params: ListDeployments }\n\t| { type: \"ListFullDeployments\", params: ListFullDeployments }\n\t| { type: \"ListCommonDeploymentExtraArgs\", params: ListCommonDeploymentExtraArgs }\n\t| { type: \"GetBuildsSummary\", params: GetBuildsSummary }\n\t| { type: \"GetBuild\", params: GetBuild }\n\t| { type: \"GetBuildActionState\", params: GetBuildActionState }\n\t| { type: \"GetBuildMonthlyStats\", params: GetBuildMonthlyStats }\n\t| { type: \"ListBuildVersions\", params: ListBuildVersions }\n\t| { type: \"GetBuildWebhookEnabled\", params: GetBuildWebhookEnabled }\n\t| { type: \"ListBuilds\", params: ListBuilds }\n\t| { type: \"ListFullBuilds\", params: ListFullBuilds }\n\t| { type: \"ListCommonBuildExtraArgs\", params: ListCommonBuildExtraArgs }\n\t| { type: \"GetReposSummary\", params: GetReposSummary }\n\t| { type: \"GetRepo\", params: GetRepo }\n\t| { type: \"GetRepoActionState\", params: GetRepoActionState }\n\t| { type: \"GetRepoWebhooksEnabled\", params: GetRepoWebhooksEnabled }\n\t| { type: \"ListRepos\", params: ListRepos }\n\t| { type: \"ListFullRepos\", params: ListFullRepos }\n\t| { type: \"GetResourceSyncsSummary\", params: GetResourceSyncsSummary }\n\t| { type: \"GetResourceSync\", params: GetResourceSync }\n\t| { type: \"GetResourceSyncActionState\", params: GetResourceSyncActionState }\n\t| { type: \"GetSyncWebhooksEnabled\", params: GetSyncWebhooksEnabled }\n\t| { type: \"ListResourceSyncs\", params: ListResourceSyncs }\n\t| { type: \"ListFullResourceSyncs\", params: ListFullResourceSyncs }\n\t| { type: \"GetBuildersSummary\", params: GetBuildersSummary }\n\t| { type: \"GetBuilder\", params: GetBuilder }\n\t| { type: \"ListBuilders\", params: ListBuilders }\n\t| { type: \"ListFullBuilders\", params: ListFullBuilders }\n\t| { type: \"GetAlertersSummary\", params: GetAlertersSummary }\n\t| { type: \"GetAlerter\", params: GetAlerter }\n\t| { type: \"ListAlerters\", params: ListAlerters }\n\t| { type: \"ListFullAlerters\", params: ListFullAlerters }\n\t| { type: \"ExportAllResourcesToToml\", params: ExportAllResourcesToToml }\n\t| { type: \"ExportResourcesToToml\", params: ExportResourcesToToml }\n\t| { type: \"GetTag\", params: GetTag }\n\t| { type: \"ListTags\", params: ListTags }\n\t| { type: \"GetUpdate\", params: GetUpdate }\n\t| { type: \"ListUpdates\", params: ListUpdates }\n\t| { type: \"ListAlerts\", params: ListAlerts }\n\t| { type: \"GetAlert\", params: GetAlert }\n\t| { type: \"GetVariable\", params: GetVariable }\n\t| { type: \"ListVariables\", params: ListVariables }\n\t| { type: \"GetGitProviderAccount\", params: GetGitProviderAccount }\n\t| { type: \"ListGitProviderAccounts\", params: ListGitProviderAccounts }\n\t| { type: \"GetDockerRegistryAccount\", params: GetDockerRegistryAccount }\n\t| { type: \"ListDockerRegistryAccounts\", params: ListDockerRegistryAccounts };\n\n/** The specific types of permission that a User or UserGroup can have on a resource. */\nexport enum SpecificPermission {\n\t/**\n\t * On **Server**\n\t * - Access the terminal apis\n\t * On **Stack / Deployment**\n\t * - Access the container exec Apis\n\t */\n\tTerminal = \"Terminal\",\n\t/**\n\t * On **Server**\n\t * - Allowed to attach Stacks, Deployments, Repos, Builders to the Server\n\t * On **Builder**\n\t * - Allowed to attach Builds to the Builder\n\t * On **Build**\n\t * - Allowed to attach Deployments to the Build\n\t */\n\tAttach = \"Attach\",\n\t/**\n\t * On **Server**\n\t * - Access the `container inspect` apis\n\t * On **Stack / Deployment**\n\t * - Access `container inspect` apis for associated containers\n\t */\n\tInspect = \"Inspect\",\n\t/**\n\t * On **Server**\n\t * - Read all container logs on the server\n\t * On **Stack / Deployment**\n\t * - Read the container logs\n\t */\n\tLogs = \"Logs\",\n\t/**\n\t * On **Server**\n\t * - Read all the processes on the host\n\t */\n\tProcesses = \"Processes\",\n}\n\nexport type UserRequest = \n\t| { type: \"PushRecentlyViewed\", params: PushRecentlyViewed }\n\t| { type: \"SetLastSeenUpdate\", params: SetLastSeenUpdate }\n\t| { type: \"CreateApiKey\", params: CreateApiKey }\n\t| { type: \"DeleteApiKey\", params: DeleteApiKey };\n\nexport type WriteRequest = \n\t| { type: \"CreateLocalUser\", params: CreateLocalUser }\n\t| { type: \"UpdateUserUsername\", params: UpdateUserUsername }\n\t| { type: \"UpdateUserPassword\", params: UpdateUserPassword }\n\t| { type: \"DeleteUser\", params: DeleteUser }\n\t| { type: \"CreateServiceUser\", params: CreateServiceUser }\n\t| { type: \"UpdateServiceUserDescription\", params: UpdateServiceUserDescription }\n\t| { type: \"CreateApiKeyForServiceUser\", params: CreateApiKeyForServiceUser }\n\t| { type: \"DeleteApiKeyForServiceUser\", params: DeleteApiKeyForServiceUser }\n\t| { type: \"CreateUserGroup\", params: CreateUserGroup }\n\t| { type: \"RenameUserGroup\", params: RenameUserGroup }\n\t| { type: \"DeleteUserGroup\", params: DeleteUserGroup }\n\t| { type: \"AddUserToUserGroup\", params: AddUserToUserGroup }\n\t| { type: \"RemoveUserFromUserGroup\", params: RemoveUserFromUserGroup }\n\t| { type: \"SetUsersInUserGroup\", params: SetUsersInUserGroup }\n\t| { type: \"SetEveryoneUserGroup\", params: SetEveryoneUserGroup }\n\t| { type: \"UpdateUserAdmin\", params: UpdateUserAdmin }\n\t| { type: \"UpdateUserBasePermissions\", params: UpdateUserBasePermissions }\n\t| { type: \"UpdatePermissionOnResourceType\", params: UpdatePermissionOnResourceType }\n\t| { type: \"UpdatePermissionOnTarget\", params: UpdatePermissionOnTarget }\n\t| { type: \"UpdateResourceMeta\", params: UpdateResourceMeta }\n\t| { type: \"CreateServer\", params: CreateServer }\n\t| { type: \"CopyServer\", params: CopyServer }\n\t| { type: \"DeleteServer\", params: DeleteServer }\n\t| { type: \"UpdateServer\", params: UpdateServer }\n\t| { type: \"RenameServer\", params: RenameServer }\n\t| { type: \"CreateNetwork\", params: CreateNetwork }\n\t| { type: \"CreateTerminal\", params: CreateTerminal }\n\t| { type: \"DeleteTerminal\", params: DeleteTerminal }\n\t| { type: \"DeleteAllTerminals\", params: DeleteAllTerminals }\n\t| { type: \"CreateStack\", params: CreateStack }\n\t| { type: \"CopyStack\", params: CopyStack }\n\t| { type: \"DeleteStack\", params: DeleteStack }\n\t| { type: \"UpdateStack\", params: UpdateStack }\n\t| { type: \"RenameStack\", params: RenameStack }\n\t| { type: \"WriteStackFileContents\", params: WriteStackFileContents }\n\t| { type: \"RefreshStackCache\", params: RefreshStackCache }\n\t| { type: \"CreateStackWebhook\", params: CreateStackWebhook }\n\t| { type: \"DeleteStackWebhook\", params: DeleteStackWebhook }\n\t| { type: \"CreateDeployment\", params: CreateDeployment }\n\t| { type: \"CopyDeployment\", params: CopyDeployment }\n\t| { type: \"CreateDeploymentFromContainer\", params: CreateDeploymentFromContainer }\n\t| { type: \"DeleteDeployment\", params: DeleteDeployment }\n\t| { type: \"UpdateDeployment\", params: UpdateDeployment }\n\t| { type: \"RenameDeployment\", params: RenameDeployment }\n\t| { type: \"CreateBuild\", params: CreateBuild }\n\t| { type: \"CopyBuild\", params: CopyBuild }\n\t| { type: \"DeleteBuild\", params: DeleteBuild }\n\t| { type: \"UpdateBuild\", params: UpdateBuild }\n\t| { type: \"RenameBuild\", params: RenameBuild }\n\t| { type: \"WriteBuildFileContents\", params: WriteBuildFileContents }\n\t| { type: \"RefreshBuildCache\", params: RefreshBuildCache }\n\t| { type: \"CreateBuildWebhook\", params: CreateBuildWebhook }\n\t| { type: \"DeleteBuildWebhook\", params: DeleteBuildWebhook }\n\t| { type: \"CreateBuilder\", params: CreateBuilder }\n\t| { type: \"CopyBuilder\", params: CopyBuilder }\n\t| { type: \"DeleteBuilder\", params: DeleteBuilder }\n\t| { type: \"UpdateBuilder\", params: UpdateBuilder }\n\t| { type: \"RenameBuilder\", params: RenameBuilder }\n\t| { type: \"CreateRepo\", params: CreateRepo }\n\t| { type: \"CopyRepo\", params: CopyRepo }\n\t| { type: \"DeleteRepo\", params: DeleteRepo }\n\t| { type: \"UpdateRepo\", params: UpdateRepo }\n\t| { type: \"RenameRepo\", params: RenameRepo }\n\t| { type: \"RefreshRepoCache\", params: RefreshRepoCache }\n\t| { type: \"CreateRepoWebhook\", params: CreateRepoWebhook }\n\t| { type: \"DeleteRepoWebhook\", params: DeleteRepoWebhook }\n\t| { type: \"CreateAlerter\", params: CreateAlerter }\n\t| { type: \"CopyAlerter\", params: CopyAlerter }\n\t| { type: \"DeleteAlerter\", params: DeleteAlerter }\n\t| { type: \"UpdateAlerter\", params: UpdateAlerter }\n\t| { type: \"RenameAlerter\", params: RenameAlerter }\n\t| { type: \"CreateProcedure\", params: CreateProcedure }\n\t| { type: \"CopyProcedure\", params: CopyProcedure }\n\t| { type: \"DeleteProcedure\", params: DeleteProcedure }\n\t| { type: \"UpdateProcedure\", params: UpdateProcedure }\n\t| { type: \"RenameProcedure\", params: RenameProcedure }\n\t| { type: \"CreateAction\", params: CreateAction }\n\t| { type: \"CopyAction\", params: CopyAction }\n\t| { type: \"DeleteAction\", params: DeleteAction }\n\t| { type: \"UpdateAction\", params: UpdateAction }\n\t| { type: \"RenameAction\", params: RenameAction }\n\t| { type: \"CreateResourceSync\", params: CreateResourceSync }\n\t| { type: \"CopyResourceSync\", params: CopyResourceSync }\n\t| { type: \"DeleteResourceSync\", params: DeleteResourceSync }\n\t| { type: \"UpdateResourceSync\", params: UpdateResourceSync }\n\t| { type: \"RenameResourceSync\", params: RenameResourceSync }\n\t| { type: \"WriteSyncFileContents\", params: WriteSyncFileContents }\n\t| { type: \"CommitSync\", params: CommitSync }\n\t| { type: \"RefreshResourceSyncPending\", params: RefreshResourceSyncPending }\n\t| { type: \"CreateSyncWebhook\", params: CreateSyncWebhook }\n\t| { type: \"DeleteSyncWebhook\", params: DeleteSyncWebhook }\n\t| { type: \"CreateTag\", params: CreateTag }\n\t| { type: \"DeleteTag\", params: DeleteTag }\n\t| { type: \"RenameTag\", params: RenameTag }\n\t| { type: \"UpdateTagColor\", params: UpdateTagColor }\n\t| { type: \"CreateVariable\", params: CreateVariable }\n\t| { type: \"UpdateVariableValue\", params: UpdateVariableValue }\n\t| { type: \"UpdateVariableDescription\", params: UpdateVariableDescription }\n\t| { type: \"UpdateVariableIsSecret\", params: UpdateVariableIsSecret }\n\t| { type: \"DeleteVariable\", params: DeleteVariable }\n\t| { type: \"CreateGitProviderAccount\", params: CreateGitProviderAccount }\n\t| { type: \"UpdateGitProviderAccount\", params: UpdateGitProviderAccount }\n\t| { type: \"DeleteGitProviderAccount\", params: DeleteGitProviderAccount }\n\t| { type: \"CreateDockerRegistryAccount\", params: CreateDockerRegistryAccount }\n\t| { type: \"UpdateDockerRegistryAccount\", params: UpdateDockerRegistryAccount }\n\t| { type: \"DeleteDockerRegistryAccount\", params: DeleteDockerRegistryAccount };\n\nexport type WsLoginMessage = \n\t| { type: \"Jwt\", params: {\n\tjwt: string;\n}}\n\t| { type: \"ApiKeys\", params: {\n\tkey: string;\n\tsecret: string;\n}};\n\n"
  },
  {
    "path": "client/core/ts/tsconfig.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"strict\": true,\n\t\t\"target\": \"ESNext\",\n\t\t\"module\": \"ESNext\",\n\t\t\"moduleResolution\": \"node\",\n\t\t\"allowSyntheticDefaultImports\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"isolatedModules\": true,\n\t\t\"outDir\": \"dist\",\n\t\t\"declaration\": true\n\t}\n}"
  },
  {
    "path": "client/periphery/rs/Cargo.toml",
    "content": "[package]\nname = \"periphery_client\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n# local\nkomodo_client.workspace = true\n# mogh\nresolver_api.workspace = true\nserror.workspace = true\n# external\ntokio-tungstenite.workspace = true\nserde_json.workspace = true\nserde_qs.workspace = true\nreqwest.workspace = true\ntracing.workspace = true\nanyhow.workspace = true\nrustls.workspace = true\ntokio.workspace = true\nserde.workspace = true"
  },
  {
    "path": "client/periphery/rs/src/api/build.rs",
    "content": "use komodo_client::entities::{\n  FileContents, repo::Repo, update::Log,\n};\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(BuildResponse)]\n#[error(serror::Error)]\npub struct Build {\n  pub build: komodo_client::entities::build::Build,\n  /// Send the linked repo if it exists.\n  pub repo: Option<Repo>,\n  /// Override registry tokens with ones sent from core.\n  /// maps (domain, account) -> token.\n  #[serde(default)]\n  pub registry_tokens: Vec<(String, String, String)>,\n  /// Propogate any secret replacers from core interpolation.\n  #[serde(default)]\n  pub replacers: Vec<(String, String)>,\n  /// Pass the commit hash to use with tagging\n  pub commit_hash: Option<String>,\n  /// Add more tags for this build in addition to the version tags.\n  #[serde(default)]\n  pub additional_tags: Vec<String>,\n}\n\npub type BuildResponse = Vec<Log>;\n\n//\n\n/// Get the dockerfile contents on the host, for builds using\n/// `files_on_host`.\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(GetDockerfileContentsOnHostResponse)]\n#[error(serror::Error)]\npub struct GetDockerfileContentsOnHost {\n  /// The name of the build\n  pub name: String,\n  /// The build path for the build.\n  pub build_path: String,\n  /// The dockerfile path for the build, relative to the build_path\n  pub dockerfile_path: String,\n}\n\npub type GetDockerfileContentsOnHostResponse = FileContents;\n\n//\n\n/// Write the dockerfile contents to the file on the host, for build using\n/// `files_on_host`.\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct WriteDockerfileContentsToHost {\n  /// The name of the build\n  pub name: String,\n  /// The build path for the build.\n  pub build_path: String,\n  /// The dockerfile path for the build, relative to the build_path\n  pub dockerfile_path: String,\n  /// The contents to write.\n  pub contents: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct PruneBuilders {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct PruneBuildx {}\n"
  },
  {
    "path": "client/periphery/rs/src/api/compose.rs",
    "content": "use komodo_client::entities::{\n  FileContents, RepoExecutionResponse, SearchCombinator,\n  repo::Repo,\n  stack::{\n    ComposeProject, Stack, StackFileDependency,\n    StackRemoteFileContents, StackServiceNames,\n  },\n  update::Log,\n};\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\n\n/// List the compose project names that are on the host.\n/// List running `docker compose ls`\n///\n/// Incoming from docker like:\n/// [{\"Name\":\"project_name\",\"Status\":\"running(1)\",\"ConfigFiles\":\"/root/compose/compose.yaml,/root/compose/compose2.yaml\"}]\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Vec<ComposeProject>)]\n#[error(serror::Error)]\npub struct ListComposeProjects {}\n\n//\n\n/// Get the compose contents on the host, for stacks using\n/// `files_on_host`.\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(GetComposeContentsOnHostResponse)]\n#[error(serror::Error)]\npub struct GetComposeContentsOnHost {\n  /// The name of the stack\n  pub name: String,\n  pub run_directory: String,\n  /// Both compose files and env / additional files, all relative to run directory.\n  pub file_paths: Vec<StackFileDependency>,\n}\n\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct GetComposeContentsOnHostResponse {\n  pub contents: Vec<StackRemoteFileContents>,\n  pub errors: Vec<FileContents>,\n}\n\n//\n\n/// The stack folder must already exist for this to work\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct GetComposeLog {\n  /// The name of the project\n  pub project: String,\n  /// Filter the logs to only ones from specific services.\n  /// If empty, will include logs from all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n  /// Pass `--tail` for only recent log contents. Max of 5000\n  #[serde(default = \"default_tail\")]\n  pub tail: u64,\n  /// Enable `--timestamps`\n  #[serde(default)]\n  pub timestamps: bool,\n}\n\nfn default_tail() -> u64 {\n  50\n}\n\n//\n\n/// The stack folder must already exist for this to work\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct GetComposeLogSearch {\n  /// The name of the project\n  pub project: String,\n  /// Filter the logs to only ones from specific services.\n  /// If empty, will include logs from all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n  /// The search terms.\n  pub terms: Vec<String>,\n  /// And: Only lines matching all terms\n  /// Or: Lines matching any one of the terms\n  #[serde(default)]\n  pub combinator: SearchCombinator,\n  /// Invert the search (search for everything not matching terms)\n  #[serde(default)]\n  pub invert: bool,\n  /// Enable `--timestamps`\n  #[serde(default)]\n  pub timestamps: bool,\n}\n\n//\n\n/// Write the compose / additional file contents to the file on the host, for stacks using\n/// `files_on_host`.\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct WriteComposeContentsToHost {\n  /// The name of the stack\n  pub name: String,\n  /// The run directory of the stack\n  pub run_directory: String,\n  /// Relative to the stack folder + run directory,\n  /// or absolute path.\n  pub file_path: String,\n  /// The contents to write.\n  pub contents: String,\n}\n\n//\n\n/// Write and commit compose contents.\n/// Only works with git repo based stacks.\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(RepoExecutionResponse)]\n#[error(serror::Error)]\npub struct WriteCommitComposeContents {\n  /// The stack to write to.\n  pub stack: Stack,\n  /// Optional linked repo.\n  pub repo: Option<Repo>,\n  /// The username of user which committed the file.\n  pub username: Option<String>,\n  /// Relative to the stack folder + run directory.\n  pub file_path: String,\n  /// The contents to write.\n  pub contents: String,\n  /// If provided, use it to login in. Otherwise check periphery local git providers.\n  pub git_token: Option<String>,\n}\n\n//\n\n/// Rewrites the compose directory, pulls any images, takes down existing containers,\n/// and runs docker compose up. Response: [ComposePullResponse]\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(ComposePullResponse)]\n#[error(serror::Error)]\npub struct ComposePull {\n  /// The stack to deploy\n  pub stack: Stack,\n  /// Filter to only pull specific services.\n  /// If empty, will pull all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n  /// The linked repo, if it exists.\n  pub repo: Option<Repo>,\n  /// If provided, use it to login in. Otherwise check periphery local git providers.\n  pub git_token: Option<String>,\n  /// If provided, use it to login in. Otherwise check periphery local registry providers.\n  pub registry_token: Option<String>,\n  /// Propogate any secret replacers from core interpolation.\n  #[serde(default)]\n  pub replacers: Vec<(String, String)>,\n}\n\n/// Response for [ComposePull]\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct ComposePullResponse {\n  /// If any of the required files are missing, they will be here.\n  pub missing_files: Vec<String>,\n  /// The error in getting remote file contents at the path, or null\n  pub remote_errors: Vec<FileContents>,\n  /// The logs produced by the pull\n  pub logs: Vec<Log>,\n}\n\n//\n\n/// docker compose up.\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(ComposeUpResponse)]\n#[error(serror::Error)]\npub struct ComposeUp {\n  /// The stack to deploy\n  pub stack: Stack,\n  /// Filter to only deploy specific services.\n  /// If empty, will deploy all services.\n  #[serde(default)]\n  pub services: Vec<String>,\n  /// The linked repo, if it exists.\n  pub repo: Option<Repo>,\n  /// If provided, use it to login in. Otherwise check periphery local registries.\n  pub git_token: Option<String>,\n  /// If provided, use it to login in. Otherwise check periphery local git providers.\n  pub registry_token: Option<String>,\n  /// Propogate any secret replacers from core interpolation.\n  #[serde(default)]\n  pub replacers: Vec<(String, String)>,\n}\n\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct ComposeUpResponse {\n  /// If any of the required files are missing, they will be here.\n  pub missing_files: Vec<String>,\n  /// The logs produced by the deploy\n  pub logs: Vec<Log>,\n  /// Whether stack was successfully deployed\n  pub deployed: bool,\n  /// The stack services.\n  ///\n  /// Note. The \"image\" is after interpolation.\n  #[serde(default)]\n  pub services: Vec<StackServiceNames>,\n  /// The deploy compose file contents if they could be acquired, or empty vec.\n  pub file_contents: Vec<StackRemoteFileContents>,\n  /// The error in getting remote file contents at the path, or null\n  pub remote_errors: Vec<FileContents>,\n  /// The output of `docker compose config` at deploy time\n  pub compose_config: Option<String>,\n  /// If its a repo based stack, will include the latest commit hash\n  pub commit_hash: Option<String>,\n  /// If its a repo based stack, will include the latest commit message\n  pub commit_message: Option<String>,\n}\n\n//\n\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct ComposeRunResponse {\n  /// Logs produced during stack write/prepare for the run\n  pub logs: Vec<Log>,\n}\n\n//\n\n/// General compose command runner\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct ComposeExecution {\n  /// The compose project name to run the execution on.\n  /// Usually its he name of the stack / folder under the `stack_dir`.\n  pub project: String,\n  /// The command in `docker compose -p {project} {command}`\n  pub command: String,\n}\n\n//\n\n/// docker compose run one-time service execution.\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct ComposeRun {\n  /// The stack to run a service for\n  pub stack: Stack,\n  /// The linked repo, if it exists.\n  pub repo: Option<Repo>,\n  /// If provided, use it to login in. Otherwise check periphery local registries.\n  pub git_token: Option<String>,\n  /// If provided, use it to login in. Otherwise check periphery local git providers.\n  pub registry_token: Option<String>,\n  /// Propogate any secret replacers from core interpolation.\n  #[serde(default)]\n  pub replacers: Vec<(String, String)>,\n\n  /// Service to run\n  pub service: String,\n  /// Command\n  #[serde(default)]\n  pub command: Option<Vec<String>>,\n  /// Do not allocate TTY\n  #[serde(default)]\n  pub no_tty: Option<bool>,\n  /// Do not start linked services\n  #[serde(default)]\n  pub no_deps: Option<bool>,\n  /// Detach container on run\n  #[serde(default)]\n  pub detach: Option<bool>,\n  /// Map service ports to the host\n  #[serde(default)]\n  pub service_ports: Option<bool>,\n  /// Extra environment variables for the run\n  #[serde(default)]\n  pub env: Option<HashMap<String, String>>,\n  /// Working directory inside the container\n  #[serde(default)]\n  pub workdir: Option<String>,\n  /// User to run as inside the container\n  #[serde(default)]\n  pub user: Option<String>,\n  /// Override the default entrypoint\n  #[serde(default)]\n  pub entrypoint: Option<String>,\n  /// Pull the image before running\n  #[serde(default)]\n  pub pull: Option<bool>,\n}\n"
  },
  {
    "path": "client/periphery/rs/src/api/container.rs",
    "content": "use komodo_client::entities::{\n  SearchCombinator, TerminationSignal,\n  deployment::Deployment,\n  docker::{\n    container::{Container, ContainerStats},\n    stats::FullContainerStats,\n  },\n  update::Log,\n};\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Container)]\n#[error(serror::Error)]\npub struct InspectContainer {\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct GetContainerLog {\n  pub name: String,\n  #[serde(default = \"default_tail\")]\n  pub tail: u64,\n  /// Enable `--timestamps`\n  #[serde(default)]\n  pub timestamps: bool,\n}\n\nfn default_tail() -> u64 {\n  50\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct GetContainerLogSearch {\n  pub name: String,\n  pub terms: Vec<String>,\n  #[serde(default)]\n  pub combinator: SearchCombinator,\n  #[serde(default)]\n  pub invert: bool,\n  /// Enable `--timestamps`\n  #[serde(default)]\n  pub timestamps: bool,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(ContainerStats)]\n#[error(serror::Error)]\npub struct GetContainerStats {\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Vec<ContainerStats>)]\n#[error(serror::Error)]\npub struct GetContainerStatsList {}\n\n//\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(FullContainerStats)]\n#[error(serror::Error)]\npub struct GetFullContainerStats {\n  pub name: String,\n}\n\n//\n\n// =======\n// ACTIONS\n// =======\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct Deploy {\n  pub deployment: Deployment,\n  pub stop_signal: Option<TerminationSignal>,\n  pub stop_time: Option<i32>,\n  /// Override registry token with one sent from core.\n  pub registry_token: Option<String>,\n  /// Propogate any secret replacers from core interpolation.\n  #[serde(default)]\n  pub replacers: Vec<(String, String)>,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct StartContainer {\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct RestartContainer {\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct PauseContainer {\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct UnpauseContainer {\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct StopContainer {\n  pub name: String,\n  pub signal: Option<TerminationSignal>,\n  pub time: Option<i32>,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct RemoveContainer {\n  pub name: String,\n  pub signal: Option<TerminationSignal>,\n  pub time: Option<i32>,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct RenameContainer {\n  pub curr_name: String,\n  pub new_name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct PruneContainers {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Vec<Log>)]\n#[error(serror::Error)]\npub struct StartAllContainers {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Vec<Log>)]\n#[error(serror::Error)]\npub struct RestartAllContainers {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Vec<Log>)]\n#[error(serror::Error)]\npub struct PauseAllContainers {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Vec<Log>)]\n#[error(serror::Error)]\npub struct UnpauseAllContainers {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Vec<Log>)]\n#[error(serror::Error)]\npub struct StopAllContainers {}\n"
  },
  {
    "path": "client/periphery/rs/src/api/git.rs",
    "content": "use std::path::PathBuf;\n\nuse komodo_client::entities::{\n  EnvironmentVar, LatestCommit, RepoExecutionArgs,\n  RepoExecutionResponse, SystemCommand, update::Log,\n};\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\n\n/// Returns `null` if not a repo\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Option<LatestCommit>)]\n#[error(serror::Error)]\npub struct GetLatestCommit {\n  pub name: String,\n  pub path: Option<String>,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(PeripheryRepoExecutionResponse)]\n#[error(serror::Error)]\npub struct CloneRepo {\n  pub args: RepoExecutionArgs,\n  /// Override git token with one sent from core.\n  pub git_token: Option<String>,\n  #[serde(default)]\n  pub environment: Vec<EnvironmentVar>,\n  /// Relative to repo root\n  #[serde(default = \"default_env_file_path\")]\n  pub env_file_path: String,\n  pub on_clone: Option<SystemCommand>,\n  pub on_pull: Option<SystemCommand>,\n  #[serde(default)]\n  pub skip_secret_interp: bool,\n  /// Propogate any secret replacers from core interpolation.\n  #[serde(default)]\n  pub replacers: Vec<(String, String)>,\n}\n\nfn default_env_file_path() -> String {\n  String::from(\".env\")\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(PeripheryRepoExecutionResponse)]\n#[error(serror::Error)]\npub struct PullRepo {\n  pub args: RepoExecutionArgs,\n  /// Override git token with one sent from core.\n  pub git_token: Option<String>,\n  #[serde(default)]\n  pub environment: Vec<EnvironmentVar>,\n  #[serde(default = \"default_env_file_path\")]\n  pub env_file_path: String,\n  pub on_pull: Option<SystemCommand>,\n  #[serde(default)]\n  pub skip_secret_interp: bool,\n  /// Propogate any secret replacers from core interpolation.\n  #[serde(default)]\n  pub replacers: Vec<(String, String)>,\n}\n\n//\n\n/// Either pull or clone depending on whether it exists.\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(PeripheryRepoExecutionResponse)]\n#[error(serror::Error)]\npub struct PullOrCloneRepo {\n  pub args: RepoExecutionArgs,\n  /// Override git token with one sent from core.\n  pub git_token: Option<String>,\n  #[serde(default)]\n  pub environment: Vec<EnvironmentVar>,\n  #[serde(default = \"default_env_file_path\")]\n  pub env_file_path: String,\n  pub on_clone: Option<SystemCommand>,\n  pub on_pull: Option<SystemCommand>,\n  #[serde(default)]\n  pub skip_secret_interp: bool,\n  /// Propogate any secret replacers from core interpolation.\n  #[serde(default)]\n  pub replacers: Vec<(String, String)>,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct PeripheryRepoExecutionResponse {\n  pub res: RepoExecutionResponse,\n  pub env_file_path: Option<PathBuf>,\n}\n\n//\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct RenameRepo {\n  pub curr_name: String,\n  pub new_name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct DeleteRepo {\n  pub name: String,\n  /// Clears\n  pub is_build: bool,\n}\n"
  },
  {
    "path": "client/periphery/rs/src/api/image.rs",
    "content": "use komodo_client::entities::{\n  docker::image::{Image, ImageHistoryResponseItem},\n  update::Log,\n};\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\n\n//\n\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Image)]\n#[error(serror::Error)]\npub struct InspectImage {\n  pub name: String,\n}\n\n//\n\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Vec<ImageHistoryResponseItem>)]\n#[error(serror::Error)]\npub struct ImageHistory {\n  pub name: String,\n}\n\n//\n\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct PullImage {\n  /// The name of the image.\n  pub name: String,\n  /// Optional account to use to pull the image\n  pub account: Option<String>,\n  /// Override registry token for account with one sent from core.\n  pub token: Option<String>,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct DeleteImage {\n  /// Id or name\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct PruneImages {}\n"
  },
  {
    "path": "client/periphery/rs/src/api/mod.rs",
    "content": "use komodo_client::entities::{\n  SystemCommand,\n  config::{DockerRegistry, GitProvider},\n  docker::{\n    container::ContainerListItem, image::ImageListItem,\n    network::NetworkListItem, volume::VolumeListItem,\n  },\n  stack::ComposeProject,\n  update::Log,\n};\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\nuse serror::Serror;\n\npub mod build;\npub mod compose;\npub mod container;\npub mod git;\npub mod image;\npub mod network;\npub mod stats;\npub mod terminal;\npub mod volume;\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(GetHealthResponse)]\n#[error(serror::Error)]\npub struct GetHealth {}\n\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetHealthResponse {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(GetVersionResponse)]\n#[error(serror::Error)]\npub struct GetVersion {}\n\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetVersionResponse {\n  pub version: String,\n}\n\n/// Returns all containers, networks, images, compose projects\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(GetDockerListsResponse)]\n#[error(serror::Error)]\npub struct GetDockerLists {}\n\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GetDockerListsResponse {\n  pub containers: Result<Vec<ContainerListItem>, Serror>,\n  pub networks: Result<Vec<NetworkListItem>, Serror>,\n  pub images: Result<Vec<ImageListItem>, Serror>,\n  pub volumes: Result<Vec<VolumeListItem>, Serror>,\n  pub projects: Result<Vec<ComposeProject>, Serror>,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(ListGitProvidersResponse)]\n#[error(serror::Error)]\npub struct ListGitProviders {}\n\npub type ListGitProvidersResponse = Vec<GitProvider>;\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(ListDockerRegistriesResponse)]\n#[error(serror::Error)]\npub struct ListDockerRegistries {}\n\npub type ListDockerRegistriesResponse = Vec<DockerRegistry>;\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Vec<String>)]\n#[error(serror::Error)]\npub struct ListSecrets {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct PruneSystem {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct RunCommand {\n  pub command: SystemCommand,\n}\n"
  },
  {
    "path": "client/periphery/rs/src/api/network.rs",
    "content": "use komodo_client::entities::{\n  docker::network::Network, update::Log,\n};\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Network)]\n#[error(serror::Error)]\npub struct InspectNetwork {\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct CreateNetwork {\n  pub name: String,\n  pub driver: Option<String>,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct DeleteNetwork {\n  /// Id or name\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct PruneNetworks {}\n"
  },
  {
    "path": "client/periphery/rs/src/api/stats.rs",
    "content": "use komodo_client::entities::stats::{\n  SystemInformation, SystemProcess, SystemStats,\n};\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(SystemInformation)]\n#[error(serror::Error)]\npub struct GetSystemInformation {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(SystemStats)]\n#[error(serror::Error)]\npub struct GetSystemStats {}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Vec<SystemProcess>)]\n#[error(serror::Error)]\npub struct GetSystemProcesses {}\n\n//\n"
  },
  {
    "path": "client/periphery/rs/src/api/terminal.rs",
    "content": "use komodo_client::{\n  api::write::TerminalRecreateMode,\n  entities::{NoData, server::TerminalInfo},\n};\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Vec<TerminalInfo>)]\n#[error(serror::Error)]\npub struct ListTerminals {}\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(NoData)]\n#[error(serror::Error)]\npub struct CreateTerminal {\n  /// The name of the terminal to create\n  pub name: String,\n  /// The shell command (eg `bash`) to init the shell.\n  ///\n  /// This can also include args:\n  /// `docker exec -it container sh`\n  #[serde(default = \"default_command\")]\n  pub command: String,\n  /// Default: `Never`\n  #[serde(default)]\n  pub recreate: TerminalRecreateMode,\n}\n\nfn default_command() -> String {\n  String::from(\"bash\")\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(NoData)]\n#[error(serror::Error)]\npub struct DeleteTerminal {\n  /// The name of the terminal to delete\n  pub terminal: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(NoData)]\n#[error(serror::Error)]\npub struct DeleteAllTerminals {}\n\n//\n\n/// Create a single use auth token to connect to periphery terminal websocket.\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(CreateTerminalAuthTokenResponse)]\n#[error(serror::Error)]\npub struct CreateTerminalAuthToken {}\n\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct CreateTerminalAuthTokenResponse {\n  pub token: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ConnectTerminalQuery {\n  /// Use [CreateTerminalAuthToken] to create a single-use\n  /// token to send in the query.\n  pub token: String,\n  /// Each periphery can keep multiple terminals open.\n  /// If a terminal with the specified name already exists,\n  /// it will be attached to. Otherwise, it will fail.\n  pub terminal: String,\n}\n\n//\n\n/// Note: The `terminal` must already exist, created by [CreateTerminal].\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ExecuteTerminalBody {\n  /// Specify the terminal to execute the command on.\n  pub terminal: String,\n  /// The command to execute.\n  pub command: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ConnectContainerExecQuery {\n  /// Use [CreateTerminalAuthToken] to create a single-use\n  /// token to send in the query.\n  pub token: String,\n  /// The name of the container to connect to.\n  pub container: String,\n  /// The shell to start inside container.\n  /// Default: `sh`\n  #[serde(default = \"default_container_shell\")]\n  pub shell: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct ExecuteContainerExecBody {\n  /// The name of the container to execute command in.\n  pub container: String,\n  /// The shell to start inside container.\n  /// Default: `sh`\n  #[serde(default = \"default_container_shell\")]\n  pub shell: String,\n  /// The command to execute.\n  pub command: String,\n}\n\nfn default_container_shell() -> String {\n  String::from(\"sh\")\n}\n"
  },
  {
    "path": "client/periphery/rs/src/api/volume.rs",
    "content": "use komodo_client::entities::{docker::volume::Volume, update::Log};\nuse resolver_api::Resolve;\nuse serde::{Deserialize, Serialize};\n\n//\n\n#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]\n#[response(Volume)]\n#[error(serror::Error)]\npub struct InspectVolume {\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct DeleteVolume {\n  /// Id or name\n  pub name: String,\n}\n\n//\n\n#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]\n#[response(Log)]\n#[error(serror::Error)]\npub struct PruneVolumes {}\n"
  },
  {
    "path": "client/periphery/rs/src/lib.rs",
    "content": "use std::{sync::OnceLock, time::Duration};\n\nuse anyhow::Context;\nuse reqwest::StatusCode;\nuse resolver_api::HasResponse;\nuse serde::{Serialize, de::DeserializeOwned};\nuse serde_json::json;\n\npub mod api;\n\nmod terminal;\n\nfn periphery_http_client() -> &'static reqwest::Client {\n  static PERIPHERY_HTTP_CLIENT: OnceLock<reqwest::Client> =\n    OnceLock::new();\n  PERIPHERY_HTTP_CLIENT.get_or_init(|| {\n    reqwest::Client::builder()\n      // Use to allow communication with Periphery self-signed certs.\n      .danger_accept_invalid_certs(true)\n      .build()\n      .expect(\"Failed to build Periphery http client\")\n  })\n}\n\npub struct PeripheryClient {\n  address: String,\n  passkey: String,\n  timeout: Duration,\n}\n\nimpl PeripheryClient {\n  pub fn new(\n    address: impl Into<String>,\n    passkey: impl Into<String>,\n    timeout: impl Into<Duration>,\n  ) -> PeripheryClient {\n    PeripheryClient {\n      address: address.into(),\n      passkey: passkey.into(),\n      timeout: timeout.into(),\n    }\n  }\n\n  // tracing will skip self, to avoid including passkey in traces\n  #[tracing::instrument(\n    name = \"PeripheryRequest\",\n    level = \"debug\",\n    skip(self)\n  )]\n  pub async fn request<T>(\n    &self,\n    request: T,\n  ) -> anyhow::Result<T::Response>\n  where\n    T: std::fmt::Debug + Serialize + HasResponse,\n    T::Response: DeserializeOwned,\n  {\n    tracing::debug!(\"running health check\");\n    self.health_check().await?;\n    tracing::debug!(\"health check passed. running inner request\");\n    self.request_inner(request, None).await\n  }\n\n  #[tracing::instrument(level = \"debug\", skip(self))]\n  pub async fn health_check(&self) -> anyhow::Result<()> {\n    self\n      .request_inner(api::GetHealth {}, Some(self.timeout))\n      .await?;\n    Ok(())\n  }\n\n  #[tracing::instrument(level = \"debug\", skip(self))]\n  async fn request_inner<T>(\n    &self,\n    request: T,\n    timeout: Option<Duration>,\n  ) -> anyhow::Result<T::Response>\n  where\n    T: std::fmt::Debug + Serialize + HasResponse,\n    T::Response: DeserializeOwned,\n  {\n    let req_type = T::req_type();\n    tracing::trace!(\n      \"sending request | type: {req_type} | body: {request:?}\"\n    );\n    let mut req = periphery_http_client()\n      .post(&self.address)\n      .json(&json!({\n        \"type\": req_type,\n        \"params\": request\n      }))\n      .header(\"authorization\", &self.passkey);\n    if let Some(timeout) = timeout {\n      req = req.timeout(timeout);\n    }\n    let res =\n      req.send().await.context(\"failed at request to periphery\")?;\n    let status = res.status();\n    tracing::debug!(\n      \"got response | type: {req_type} | {status} | response: {res:?}\",\n    );\n    if status == StatusCode::OK {\n      tracing::debug!(\"response ok, deserializing\");\n      res.json().await.with_context(|| format!(\n        \"failed to parse response to json | type: {req_type} | request: {request:?}\"\n      ))\n    } else {\n      tracing::debug!(\"response is non-200\");\n\n      let text = res\n        .text()\n        .await\n        .context(\"failed to convert response to text\")?;\n\n      tracing::debug!(\"got response text, deserializing error\");\n\n      let error = serror::deserialize_error(text).context(status);\n\n      Err(error)\n    }\n  }\n}\n"
  },
  {
    "path": "client/periphery/rs/src/terminal.rs",
    "content": "use std::sync::Arc;\n\nuse anyhow::Context;\nuse komodo_client::terminal::TerminalStreamResponse;\nuse reqwest::RequestBuilder;\nuse rustls::{ClientConfig, client::danger::ServerCertVerifier};\nuse tokio::net::TcpStream;\nuse tokio_tungstenite::{Connector, MaybeTlsStream, WebSocketStream};\n\nuse crate::{PeripheryClient, api::terminal::*};\n\nimpl PeripheryClient {\n  /// Handles ws connect and login.\n  /// Does not handle reconnect.\n  pub async fn connect_terminal(\n    &self,\n    terminal: String,\n  ) -> anyhow::Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {\n    tracing::trace!(\n      \"request | type: ConnectTerminal | terminal name: {terminal}\",\n    );\n\n    let token = self\n      .request(CreateTerminalAuthToken {})\n      .await\n      .context(\"Failed to create terminal auth token\")?;\n\n    let query_str = serde_qs::to_string(&ConnectTerminalQuery {\n      token: token.token,\n      terminal,\n    })\n    .context(\"Failed to serialize query string\")?;\n\n    let url = format!(\n      \"{}/terminal?{query_str}\",\n      self.address.replacen(\"http\", \"ws\", 1)\n    );\n\n    connect_websocket(&url).await\n  }\n\n  /// Executes command on specified terminal,\n  /// and streams the response ending in [KOMODO_EXIT_CODE][komodo_client::entities::KOMODO_EXIT_CODE]\n  /// sentinal value as the expected final line of the stream.\n  ///\n  /// Example final line:\n  /// ```text\n  /// __KOMODO_EXIT_CODE:0\n  /// ```\n  ///\n  /// This means the command exited with code 0 (success).\n  ///\n  /// If this value is NOT the final item before stream closes, it means\n  /// the terminal exited mid command, before giving status. Example: running `exit`.\n  #[tracing::instrument(level = \"debug\", skip(self))]\n  pub async fn execute_terminal(\n    &self,\n    terminal: String,\n    command: String,\n  ) -> anyhow::Result<TerminalStreamResponse> {\n    tracing::trace!(\n      \"sending request | type: ExecuteTerminal | terminal name: {terminal} | command: {command}\",\n    );\n    let req = crate::periphery_http_client()\n      .post(format!(\"{}/terminal/execute\", self.address))\n      .json(&ExecuteTerminalBody { terminal, command })\n      .header(\"authorization\", &self.passkey);\n    terminal_stream_response(req).await\n  }\n\n  /// Handles ws connect and login.\n  /// Does not handle reconnect.\n  pub async fn connect_container_exec(\n    &self,\n    container: String,\n    shell: String,\n  ) -> anyhow::Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {\n    tracing::trace!(\n      \"request | type: ConnectContainerExec | container name: {container} | shell: {shell}\",\n    );\n\n    let token = self\n      .request(CreateTerminalAuthToken {})\n      .await\n      .context(\"Failed to create terminal auth token\")?;\n\n    let query_str = serde_qs::to_string(&ConnectContainerExecQuery {\n      token: token.token,\n      container,\n      shell,\n    })\n    .context(\"Failed to serialize query string\")?;\n\n    let url = format!(\n      \"{}/terminal/container?{query_str}\",\n      self.address.replacen(\"http\", \"ws\", 1)\n    );\n\n    connect_websocket(&url).await\n  }\n\n  /// Executes command on specified container,\n  /// and streams the response ending in [KOMODO_EXIT_CODE][komodo_client::entities::KOMODO_EXIT_CODE]\n  /// sentinal value as the expected final line of the stream.\n  ///\n  /// Example final line:\n  /// ```text\n  /// __KOMODO_EXIT_CODE:0\n  /// ```\n  ///\n  /// This means the command exited with code 0 (success).\n  ///\n  /// If this value is NOT the final item before stream closes, it means\n  /// the container shell exited mid command, before giving status. Example: running `exit`.\n  #[tracing::instrument(level = \"debug\", skip(self))]\n  pub async fn execute_container_exec(\n    &self,\n    container: String,\n    shell: String,\n    command: String,\n  ) -> anyhow::Result<TerminalStreamResponse> {\n    tracing::trace!(\n      \"sending request | type: ExecuteContainerExec | container: {container} | shell: {shell} | command: {command}\",\n    );\n    let req = crate::periphery_http_client()\n      .post(format!(\"{}/terminal/execute/container\", self.address))\n      .json(&ExecuteContainerExecBody {\n        container,\n        shell,\n        command,\n      })\n      .header(\"authorization\", &self.passkey);\n    terminal_stream_response(req).await\n  }\n}\n\nasync fn connect_websocket(\n  url: &str,\n) -> anyhow::Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {\n  let (stream, _) = if url.starts_with(\"wss\") {\n    tokio_tungstenite::connect_async_tls_with_config(\n      url,\n      None,\n      false,\n      Some(Connector::Rustls(Arc::new(\n        ClientConfig::builder()\n          .dangerous()\n          .with_custom_certificate_verifier(Arc::new(\n            InsecureVerifier,\n          ))\n          .with_no_client_auth(),\n      ))),\n    )\n    .await\n    .with_context(|| {\n      format!(\"failed to connect to websocket | url: {url}\")\n    })?\n  } else {\n    tokio_tungstenite::connect_async(url).await.with_context(\n      || format!(\"failed to connect to websocket | url: {url}\"),\n    )?\n  };\n\n  Ok(stream)\n}\n\nasync fn terminal_stream_response(\n  req: RequestBuilder,\n) -> anyhow::Result<TerminalStreamResponse> {\n  let res =\n    req.send().await.context(\"Failed at request to periphery\")?;\n  let status = res.status();\n  tracing::debug!(\n    \"got response | type: ExecuteTerminal | {status} | response: {res:?}\",\n  );\n  if status.is_success() {\n    Ok(TerminalStreamResponse(res))\n  } else {\n    tracing::debug!(\"response is non-200\");\n\n    let text = res\n      .text()\n      .await\n      .context(\"Failed to convert response to text\")?;\n\n    tracing::debug!(\"got response text, deserializing error\");\n\n    let error = serror::deserialize_error(text).context(status);\n\n    Err(error)\n  }\n}\n\n#[derive(Debug)]\nstruct InsecureVerifier;\n\nimpl ServerCertVerifier for InsecureVerifier {\n  fn verify_server_cert(\n    &self,\n    _end_entity: &rustls::pki_types::CertificateDer<'_>,\n    _intermediates: &[rustls::pki_types::CertificateDer<'_>],\n    _server_name: &rustls::pki_types::ServerName<'_>,\n    _ocsp_response: &[u8],\n    _now: rustls::pki_types::UnixTime,\n  ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error>\n  {\n    Ok(rustls::client::danger::ServerCertVerified::assertion())\n  }\n\n  fn verify_tls12_signature(\n    &self,\n    _message: &[u8],\n    _cert: &rustls::pki_types::CertificateDer<'_>,\n    _dss: &rustls::DigitallySignedStruct,\n  ) -> Result<\n    rustls::client::danger::HandshakeSignatureValid,\n    rustls::Error,\n  > {\n    Ok(rustls::client::danger::HandshakeSignatureValid::assertion())\n  }\n\n  fn verify_tls13_signature(\n    &self,\n    _message: &[u8],\n    _cert: &rustls::pki_types::CertificateDer<'_>,\n    _dss: &rustls::DigitallySignedStruct,\n  ) -> Result<\n    rustls::client::danger::HandshakeSignatureValid,\n    rustls::Error,\n  > {\n    Ok(rustls::client::danger::HandshakeSignatureValid::assertion())\n  }\n\n  fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {\n    vec![\n      rustls::SignatureScheme::RSA_PKCS1_SHA1,\n      rustls::SignatureScheme::ECDSA_SHA1_Legacy,\n      rustls::SignatureScheme::RSA_PKCS1_SHA256,\n      rustls::SignatureScheme::ECDSA_NISTP256_SHA256,\n      rustls::SignatureScheme::RSA_PKCS1_SHA384,\n      rustls::SignatureScheme::ECDSA_NISTP384_SHA384,\n      rustls::SignatureScheme::RSA_PKCS1_SHA512,\n      rustls::SignatureScheme::ECDSA_NISTP521_SHA512,\n      rustls::SignatureScheme::RSA_PSS_SHA256,\n      rustls::SignatureScheme::RSA_PSS_SHA384,\n      rustls::SignatureScheme::RSA_PSS_SHA512,\n      rustls::SignatureScheme::ED25519,\n      rustls::SignatureScheme::ED448,\n    ]\n  }\n}\n"
  },
  {
    "path": "compose/compose.env",
    "content": "####################################\n# 🦎 KOMODO COMPOSE - VARIABLES 🦎 #\n####################################\n\n## These compose variables can be used with all Komodo deployment options.\n## Pass these variables to the compose up command using `--env-file komodo/compose.env`.\n## Additionally, they are passed to both Komodo Core and Komodo Periphery with `env_file: ./compose.env`,\n## so you can pass any additional environment variables to Core / Periphery directly in this file as well.\n\n## Stick to a specific version, or use `latest`\nCOMPOSE_KOMODO_IMAGE_TAG=latest\n## Store dated database backups on the host - https://komo.do/docs/setup/backup\nCOMPOSE_KOMODO_BACKUPS_PATH=/etc/komodo/backups\n\n## DB credentials\nKOMODO_DB_USERNAME=admin\nKOMODO_DB_PASSWORD=admin\n\n## Configure a secure passkey to authenticate between Core / Periphery.\nKOMODO_PASSKEY=a_random_passkey\n\n## Set your time zone for schedules\n## https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\nTZ=Etc/UTC\n\n#=-------------------------=#\n#= Komodo Core Environment =#\n#=-------------------------=#\n\n## Full variable list + descriptions are available here:\n## 🦎 https://github.com/moghtech/komodo/blob/main/config/core.config.toml 🦎\n\n## Note. Secret variables also support `${VARIABLE}_FILE` syntax to pass docker compose secrets.\n## Docs: https://docs.docker.com/compose/how-tos/use-secrets/#examples\n\n## Used for Oauth / Webhook url suggestion / Caddy reverse proxy.\nKOMODO_HOST=https://demo.komo.do\n## Displayed in the browser tab.\nKOMODO_TITLE=Komodo\n## Create a server matching this address as the \"first server\".\n## Use `https://host.docker.internal:8120` when using systemd-managed Periphery.\nKOMODO_FIRST_SERVER=https://periphery:8120\n## Give the first server a custom name.\nKOMODO_FIRST_SERVER_NAME=Local\n## Make all buttons just double-click, rather than the full confirmation dialog.\nKOMODO_DISABLE_CONFIRM_DIALOG=false\n\n## Rate Komodo polls your servers for\n## status / container status / system stats / alerting.\n## Options: 1-sec, 5-sec, 15-sec, 1-min, 5-min, 15-min\n## Default: 15-sec\nKOMODO_MONITORING_INTERVAL=\"15-sec\"\n## Interval at which to poll Resources for any updates / automated actions.\n## Options: 15-min, 1-hr, 2-hr, 6-hr, 12-hr, 1-day\n## Default: 1-hr\nKOMODO_RESOURCE_POLL_INTERVAL=\"1-hr\"\n\n## Used to auth incoming webhooks. Alt: KOMODO_WEBHOOK_SECRET_FILE\nKOMODO_WEBHOOK_SECRET=a_random_secret\n## Used to generate jwt. Alt: KOMODO_JWT_SECRET_FILE\nKOMODO_JWT_SECRET=a_random_jwt_secret\n## Time to live for jwt tokens.\n## Options: 1-hr, 12-hr, 1-day, 3-day, 1-wk, 2-wk\nKOMODO_JWT_TTL=\"1-day\"\n\n## Enable login with username + password.\nKOMODO_LOCAL_AUTH=true\n## Set the initial admin username created upon first launch.\n## Comment out to disable initial user creation,\n## and create first user using signup button.\nKOMODO_INIT_ADMIN_USERNAME=admin\n## Set the initial admin password\nKOMODO_INIT_ADMIN_PASSWORD=changeme\n## Disable new user signups.\nKOMODO_DISABLE_USER_REGISTRATION=false\n## All new logins are auto enabled\nKOMODO_ENABLE_NEW_USERS=false\n## Disable non-admins from creating new resources.\nKOMODO_DISABLE_NON_ADMIN_CREATE=false\n## Allows all users to have Read level access to all resources.\nKOMODO_TRANSPARENT_MODE=false\n\n## Prettier logging with empty lines between logs\nKOMODO_LOGGING_PRETTY=false\n## More human readable logging of startup config (multi-line)\nKOMODO_PRETTY_STARTUP_CONFIG=false\n\n## OIDC Login\nKOMODO_OIDC_ENABLED=false\n## Must reachable from Komodo Core container\n# KOMODO_OIDC_PROVIDER=https://oidc.provider.internal/application/o/komodo\n## Change the host to one reachable be reachable by users (optional if it is the same as above).\n## DO NOT include the `path` part of the URL.\n# KOMODO_OIDC_REDIRECT_HOST=https://oidc.provider.external\n## Your OIDC client id\n# KOMODO_OIDC_CLIENT_ID= # Alt: KOMODO_OIDC_CLIENT_ID_FILE\n## Your OIDC client secret.\n## If your provider supports PKCE flow, this can be ommitted.\n# KOMODO_OIDC_CLIENT_SECRET= # Alt: KOMODO_OIDC_CLIENT_SECRET_FILE\n## Make usernames the full email.\n## Note. This does not work for all OIDC providers.\n# KOMODO_OIDC_USE_FULL_EMAIL=true\n## Add additional trusted audiences for token claims verification.\n## Supports comma separated list, and passing with _FILE (for compose secrets).\n# KOMODO_OIDC_ADDITIONAL_AUDIENCES=abc,123 # Alt: KOMODO_OIDC_ADDITIONAL_AUDIENCES_FILE\n\n## Github Oauth\nKOMODO_GITHUB_OAUTH_ENABLED=false\n# KOMODO_GITHUB_OAUTH_ID= # Alt: KOMODO_GITHUB_OAUTH_ID_FILE\n# KOMODO_GITHUB_OAUTH_SECRET= # Alt: KOMODO_GITHUB_OAUTH_SECRET_FILE\n\n## Google Oauth\nKOMODO_GOOGLE_OAUTH_ENABLED=false\n# KOMODO_GOOGLE_OAUTH_ID= # Alt: KOMODO_GOOGLE_OAUTH_ID_FILE\n# KOMODO_GOOGLE_OAUTH_SECRET= # Alt: KOMODO_GOOGLE_OAUTH_SECRET_FILE\n\n## Aws - Used to launch Builder instances.\nKOMODO_AWS_ACCESS_KEY_ID= # Alt: KOMODO_AWS_ACCESS_KEY_ID_FILE\nKOMODO_AWS_SECRET_ACCESS_KEY= # Alt: KOMODO_AWS_SECRET_ACCESS_KEY_FILE\n\n#=------------------------------=#\n#= Komodo Periphery Environment =#\n#=------------------------------=#\n\n## Full variable list + descriptions are available here:\n## 🦎 https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml 🦎\n\n## Specify the root directory used by Periphery agent.\nPERIPHERY_ROOT_DIRECTORY=/etc/komodo\n\n## Periphery passkeys must include KOMODO_PASSKEY to authenticate.\nPERIPHERY_PASSKEYS=${KOMODO_PASSKEY}\n\n## Specify whether to disable the terminals feature\n## and disallow remote shell access (inside the Periphery container).\nPERIPHERY_DISABLE_TERMINALS=false\n\n## Enable SSL using self signed certificates.\n## Connect to Periphery at https://address:8120.\nPERIPHERY_SSL_ENABLED=true\n\n## If the disk size is overreporting, can use one of these to \n## whitelist / blacklist the disks to filter them, whichever is easier.\n## Accepts comma separated list of paths.\n## Usually whitelisting just /etc/hostname gives correct size.\nPERIPHERY_INCLUDE_DISK_MOUNTS=/etc/hostname\n# PERIPHERY_EXCLUDE_DISK_MOUNTS=/snap,/etc/repos\n\n## Prettier logging with empty lines between logs\nPERIPHERY_LOGGING_PRETTY=false\n## More human readable logging of startup config (multi-line)\nPERIPHERY_PRETTY_STARTUP_CONFIG=false"
  },
  {
    "path": "compose/ferretdb.compose.yaml",
    "content": "###################################\n# 🦎 KOMODO COMPOSE - FERRETDB 🦎 #\n###################################\n\n## This compose file will deploy:\n##   1. Postgres + FerretDB Mongo adapter (https://www.ferretdb.com)\n##   2. Komodo Core\n##   3. Komodo Periphery\n\nservices:\n  postgres:\n    # 🚨 Pin to a specific version. Updates can be breaking.\n    # https://github.com/FerretDB/documentdb/pkgs/container/postgres-documentdb\n    image: ghcr.io/ferretdb/postgres-documentdb\n    labels:\n      komodo.skip: # Prevent Komodo from stopping with StopAllContainers\n    restart: unless-stopped\n    # ports:\n    #   - 5432:5432\n    volumes:\n      - postgres-data:/var/lib/postgresql/data\n    environment:\n      POSTGRES_USER: ${KOMODO_DB_USERNAME}\n      POSTGRES_PASSWORD: ${KOMODO_DB_PASSWORD}\n      POSTGRES_DB: postgres\n\n  ferretdb:\n    # 🚨 Pin to a specific version. Updates can be breaking.\n    # https://github.com/FerretDB/FerretDB/pkgs/container/ferretdb\n    image: ghcr.io/ferretdb/ferretdb\n    labels:\n      komodo.skip: # Prevent Komodo from stopping with StopAllContainers\n    restart: unless-stopped\n    depends_on:\n      - postgres\n    # ports:\n    #   - 27017:27017\n    volumes:\n      - ferretdb-state:/state\n    environment:\n      FERRETDB_POSTGRESQL_URL: postgres://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@postgres:5432/postgres\n  \n  core:\n    image: ghcr.io/moghtech/komodo-core:${COMPOSE_KOMODO_IMAGE_TAG:-latest}\n    labels:\n      komodo.skip: # Prevent Komodo from stopping with StopAllContainers\n    restart: unless-stopped\n    depends_on:\n      - ferretdb\n    ports:\n      - 9120:9120\n    env_file: ./compose.env\n    environment:\n      KOMODO_DATABASE_ADDRESS: ferretdb:27017\n      KOMODO_DATABASE_USERNAME: ${KOMODO_DB_USERNAME}\n      KOMODO_DATABASE_PASSWORD: ${KOMODO_DB_PASSWORD}\n    volumes:\n      ## Store dated backups of the database - https://komo.do/docs/setup/backup\n      - ${COMPOSE_KOMODO_BACKUPS_PATH}:/backups\n      ## Store sync files on server\n      # - /path/to/syncs:/syncs\n      ## Optionally mount a custom core.config.toml\n      # - /path/to/core.config.toml:/config/config.toml\n    ## Allows for systemd Periphery connection at \n    ## \"https://host.docker.internal:8120\"\n    # extra_hosts:\n    #   - host.docker.internal:host-gateway\n\n  ## Deploy Periphery container using this block,\n  ## or deploy the Periphery binary with systemd using \n  ## https://github.com/moghtech/komodo/tree/main/scripts\n  periphery:\n    image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}\n    labels:\n      komodo.skip: # Prevent Komodo from stopping with StopAllContainers\n    restart: unless-stopped\n    env_file: ./compose.env\n    volumes:\n      ## Mount external docker socket\n      - /var/run/docker.sock:/var/run/docker.sock\n      ## Allow Periphery to see processes outside of container\n      - /proc:/proc\n      ## Specify the Periphery agent root directory.\n      ## Must be the same inside and outside the container,\n      ## or docker will get confused. See https://github.com/moghtech/komodo/discussions/180.\n      ## Default: /etc/komodo.\n      - ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}\n\nvolumes:\n  # Postgres\n  postgres-data:\n  # FerretDB\n  ferretdb-state:"
  },
  {
    "path": "compose/mongo.compose.yaml",
    "content": "################################\n# 🦎 KOMODO COMPOSE - MONGO 🦎 #\n################################\n\n## This compose file will deploy:\n##   1. MongoDB\n##   2. Komodo Core\n##   3. Komodo Periphery\n\nservices:\n  mongo:\n    image: mongo\n    labels:\n      komodo.skip: # Prevent Komodo from stopping with StopAllContainers\n    command: --quiet --wiredTigerCacheSizeGB 0.25\n    restart: unless-stopped\n    # ports:\n    #   - 27017:27017\n    volumes:\n      - mongo-data:/data/db\n      - mongo-config:/data/configdb\n    environment:\n      MONGO_INITDB_ROOT_USERNAME: ${KOMODO_DB_USERNAME}\n      MONGO_INITDB_ROOT_PASSWORD: ${KOMODO_DB_PASSWORD}\n  \n  core:\n    image: ghcr.io/moghtech/komodo-core:${COMPOSE_KOMODO_IMAGE_TAG:-latest}\n    labels:\n      komodo.skip: # Prevent Komodo from stopping with StopAllContainers\n    restart: unless-stopped\n    depends_on:\n      - mongo\n    ports:\n      - 9120:9120\n    env_file: ./compose.env\n    environment:\n      KOMODO_DATABASE_ADDRESS: mongo:27017\n      KOMODO_DATABASE_USERNAME: ${KOMODO_DB_USERNAME}\n      KOMODO_DATABASE_PASSWORD: ${KOMODO_DB_PASSWORD}\n    volumes:\n      ## Store dated backups of the database - https://komo.do/docs/setup/backup\n      - ${COMPOSE_KOMODO_BACKUPS_PATH}:/backups\n      ## Store sync files on server\n      # - /path/to/syncs:/syncs\n      ## Optionally mount a custom core.config.toml\n      # - /path/to/core.config.toml:/config/config.toml\n    ## Allows for systemd Periphery connection at \n    ## \"https://host.docker.internal:8120\"\n    # extra_hosts:\n    #   - host.docker.internal:host-gateway\n\n  ## Deploy Periphery container using this block,\n  ## or deploy the Periphery binary with systemd using \n  ## https://github.com/moghtech/komodo/tree/main/scripts\n  periphery:\n    image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}\n    labels:\n      komodo.skip: # Prevent Komodo from stopping with StopAllContainers\n    restart: unless-stopped\n    env_file: ./compose.env\n    volumes:\n      ## Mount external docker socket\n      - /var/run/docker.sock:/var/run/docker.sock\n      ## Allow Periphery to see processes outside of container\n      - /proc:/proc\n      ## Specify the Periphery agent root directory.\n      ## Must be the same inside and outside the container,\n      ## or docker will get confused. See https://github.com/moghtech/komodo/discussions/180.\n      ## Default: /etc/komodo.\n      - ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}\n\nvolumes:\n  # Mongo\n  mongo-data:\n  mongo-config:"
  },
  {
    "path": "compose/periphery.compose.yaml",
    "content": "####################################\n# 🦎 KOMODO COMPOSE - PERIPHERY 🦎 #\n####################################\n\n## This compose file will deploy:\n##   1. Komodo Periphery\n\nservices:\n  periphery:\n    image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}\n    labels:\n      komodo.skip: # Prevent Komodo from stopping with StopAllContainers\n    restart: unless-stopped\n    ## https://komo.do/docs/connect-servers#configuration\n    environment:\n      PERIPHERY_ROOT_DIRECTORY: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}\n      ## Pass the same passkey as used by the Komodo Core connecting to this Periphery agent.\n      PERIPHERY_PASSKEYS: abc123\n      ## Make server run over https\n      PERIPHERY_SSL_ENABLED: true\n      ## Specify whether to disable the terminals feature\n      ## and disallow remote shell access (inside the Periphery container).\n      PERIPHERY_DISABLE_TERMINALS: false\n      ## If the disk size is overreporting, can use one of these to \n      ## whitelist / blacklist the disks to filter them, whichever is easier.\n      ## Accepts comma separated list of paths.\n      ## Usually whitelisting just /etc/hostname gives correct size for single root disk.\n      PERIPHERY_INCLUDE_DISK_MOUNTS: /etc/hostname\n      # PERIPHERY_EXCLUDE_DISK_MOUNTS: /snap,/etc/repos\n    volumes:\n      ## Mount external docker socket\n      - /var/run/docker.sock:/var/run/docker.sock\n      ## Allow Periphery to see processes outside of container\n      - /proc:/proc\n      ## Specify the Periphery agent root directory.\n      ## Must be the same inside and outside the container,\n      ## or docker will get confused. See https://github.com/moghtech/komodo/discussions/180.\n      ## Default: /etc/komodo.\n      - ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}\n    ## If periphery is being run remote from the core server, ports need to be exposed\n    # ports:\n    #   - 8120:8120\n    ## If you want to use a custom periphery config file, use command to pass it to periphery.\n    # command: periphery --config-path ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/periphery.config.toml"
  },
  {
    "path": "config/core.config.toml",
    "content": "###########################\n# 🦎 KOMODO CORE CONFIG 🦎 #\n###########################\n\n## This is the offical \"Default\" config file for Komodo Core.\n## It serves as documentation for the meaning of the fields.\n## It is located at `https://github.com/moghtech/komodo/blob/main/config/core.config.toml`.\n\n## All fields with a \"Default\" provided are optional. If they are\n## left out of the file, the \"Default\" value will be used.\n\n## This file is bundled into the official image, `ghcr.io/moghtech/komodo-core`,\n## as the default config at `/config/.default.config.toml`.\n## Komodo Core can start with no external config file mounted.\n\n## Most fields can also be configured using environment variables.\n## Environment variables will override values set in this file.\n\n## Can also use JSON or YAML if preferred. You can convert here:\n##   - YAML: https://it-tools.tech/toml-to-yaml\n##   - JSON: https://it-tools.tech/toml-to-json\n\n## This will be the document title on the web page.\n## Env: KOMODO_TITLE\n## Default: 'Komodo'\ntitle = \"Komodo\"\n\n## This should be the url used to access Komodo in browser, potentially behind DNS.\n## Eg https://komodo.example.com or http://12.34.56.78:9120. This should match the address configured in your Oauth app.\n## Env: KOMODO_HOST\n## Required, no default.\nhost = \"https://komodo.example.com\"\n\n## The port the core system will run on.\n## Env: KOMODO_PORT\n## Default: 9120\nport = 9120\n\n## The IP address the core server will bind to.\n## The default will allow it to accept external IPv4 and IPv6 connections.\n## Env: KOMODO_BIND_IP\n## Default: [::]\nbind_ip = \"[::]\"\n\n## This is the token used to authenticate core requests to periphery.\n## Ensure this matches a passkey in the connected periphery configs.\n## If the periphery servers don't have passkeys configured, this doesn't need to be changed.\n## Env: KOMODO_PASSKEY or KOMODO_PASSKEY_FILE\n## Required, no default\npasskey = \"default-passkey-changeme\"\n\n## Ensure a server with this address exists on Core\n## upon first startup. Example: `https://periphery:8120`\n## Env: KOMODO_FIRST_SERVER\n## Optional, no default.\n# first_server = \"\"\n\n## Give the first server a custom name.\n## Env: KOMODO_FIRST_SERVER_NAME\n## Default: Local\nfirst_server_name = \"Local\"\n\n## Disables write support on resources in the UI.\n## This protects users that that would normally have write priviledges during their UI usage,\n## when they intend to fully rely on ResourceSyncs to manage config.\n## Env: KOMODO_UI_WRITE_DISABLED\n## Default: false\nui_write_disabled = false\n\n## Disables the confirm dialogs on all actions. All buttons will now be double-click.\n## Useful when only having http connection to core, as UI quick-copy button won't work.\n## Env: KOMODO_DISABLE_CONFIRM_DIALOG\n## Default: false\ndisable_confirm_dialog = false\n\n## Disables UI websocket automatic reconnection.\n## Users will still be able to trigger reconnect by clicking the connection indicator.\n## Env: KOMODO_DISABLE_WEBSOCKET_RECONNECT\n## Default: false\ndisable_websocket_reconnect = false\n\n\n## Disable init system resource creation on fresh Komodo launch.\n## These include the 'Backup Core Database' and 'Global Auto Update' procedures.\n## Env: KOMODO_DISABLE_INIT_RESOURCES\n## Default: false\ndisable_init_resources = false\n\n## Configure the directory for sync files (inside the container).\n## There shouldn't be a need to change this, just mount a volume.\n## Env: KOMODO_SYNC_DIRECTORY\n## Default: /syncs\nsync_directory = \"/syncs\"\n\n## Configure the repo directory (inside the container).\n## There shouldn't be a need to change this, just mount a volume.\n## Env: KOMODO_REPO_DIRECTORY\n## Default: /repo-cache\nrepo_directory = \"/repo-cache\"\n\n## Configure the action directory (inside the container).\n## There shouldn't be a need to change this, or even mount a volume.\n## Env: KOMODO_ACTION_DIRECTORY\n## Default: /action-cache\naction_directory = \"/action-cache\"\n\n## Interface to use as default route in multi-NIC environments.\n## Env: KOMODO_INTERNET_INTERFACE\n## Example: \"eth1\"\n## Optional, no default.\ninternet_interface = \"\"\n\n################\n# AUTH / LOGIN #\n################\n\n## Allow user login with a username / password.\n## The password will be hashed and stored in the db for login comparison.\n##\n## NOTE:\n## Komodo has no API to recover account logins, but if this happens you can doctor the database using Mongo Compass.\n## Create a new Komodo user (Sign Up button), login to the database with Compass, note down your old users username and _id.\n## Then delete the old user, and update the new user to have the same username and _id. \n## Make sure to set `enabled: true` and maybe `admin: true` on the new user as well, while using Compass.\n##\n## Env: KOMODO_LOCAL_AUTH\n## Default: false\nlocal_auth = false\n\n## Initialize the first admin user when starting up Komodo for the first time.\n## Env: KOMODO_INIT_ADMIN_USERNAME or KOMODO_INIT_ADMIN_USERNAME_FILE\n## Default: None\n# init_admin_username = \"admin\"\n\n## Set password for first admin user\n## Env: KOMODO_INIT_ADMIN_PASSWORD or KOMODO_INIT_ADMIN_PASSWORD_FILE\n## Default: changeme\ninit_admin_password = \"changeme\"\n\n## Normally new users will be registered, but not enabled until an Admin enables them.\n## With `disable_user_registration = true`, only the first user to log in will registered as a user.\n## Env: KOMODO_DISABLE_USER_REGISTRATION\n## Default: false\ndisable_user_registration = false\n\n## New users will be automatically enabled when they sign up.\n## Otherwise, new users will be disabled on first login.\n## The first user to login will always be enabled on creation.\n## Env: KOMODO_ENABLE_NEW_USERS\n## Default: false\nenable_new_users = false\n\n## Allows all users to have Read level access to all resources.\n## Env: KOMODO_TRANSPARENT_MODE\n## Default: false\ntransparent_mode = false\n\n## Normally all enabled users can create resources.\n## If `disable_non_admin_create = true`, only admin users can create resources.\n## Env: KOMODO_DISABLE_NON_ADMIN_CREATE\n## Default: false\ndisable_non_admin_create = false\n\n## Normally users can update their username / password using the API.\n## This will disable this ability for specific users or all users.\n## Example:\n##  - `lock_login_credentials_for = []` will allow all users to update username / password.\n##  - `lock_login_credentials_for = [\"demo\"]` will block the demo user from doing so.\n##  - `lock_login_credentials_for = [\"__ALL__\"]` will block all users.\n## Env: KOMODO_LOCK_LOGIN_CREDENTIALS_FOR\n## Default: empty list\nlock_login_credentials_for = []\n\n## Optionally provide a specific jwt secret.\n## Passing nothing or an empty string will cause one to be generated on every startup.\n## This means users will have to log in again if Komodo restarts.\n## Env: KOMODO_JWT_SECRET or KOMODO_JWT_SECRET_FILE\n## Default: empty string, meaning a random secret will be generated at startup.\njwt_secret = \"\"\n\n## Specify how long a user can stay logged in before they have to log in again.\n## All jwts are invalidated on application restart unless `jwt_secret` is set.\n## Env: KOMODO_JWT_TTL\n## Options: https://docs.rs/komodo_client/latest/komodo_client/entities/enum.Timelength.html\n## Default: 1-day. \njwt_ttl = \"1-day\"\n\n#############\n# OIDC Auth #\n#############\n\n## Enable logins with configured OIDC provider.\n## Env: KOMODO_OIDC_ENABLED\n## Default: false\noidc_enabled = false\n\n## Give the provider address.\n##\n## The path, ie /application/o/komodo for Authentik,\n## is provider and configuration specific.\n##\n## Note. this address must be reachable from Komodo Core container.\n##\n## Env: KOMODO_OIDC_PROVIDER\n## Optional, no default.\noidc_provider = \"https://oidc.provider.internal/application/o/komodo\"\n\n## Configure OIDC user redirect host.\n##\n## This is the host address users are redirected to in their browser,\n## and may be different from `oidc_provider` host depending on your networking.\n## If not provided (or empty string \"\"), the `oidc_provider` will be used.\n##\n## Note. DO NOT include the `path` part of the URL.\n## Example: `https://oidc.provider.external`\n##\n## Env: KOMODO_OIDC_REDIRECT_HOST\n## Optional, no default.\noidc_redirect_host = \"\"\n\n## Set the OIDC Client ID.\n## Env: KOMODO_OIDC_CLIENT_ID or KOMODO_OIDC_CLIENT_ID_FILE\noidc_client_id = \"\"\n\n## Set the OIDC Client Secret.\n## If the OIDC provider supports PKCE-only flow,\n## the client secret is not necessary and can be ommitted or left empty.\n## Env: KOMODO_OIDC_CLIENT_SECRET or KOMODO_OIDC_CLIENT_SECRET_FILE\noidc_client_secret = \"\"\n\n## If true, use the full email for usernames.\n## Otherwise, the @address will be stripped,\n## making usernames more concise.\n## Note. This does not work for all OIDC providers.\n## Env: KOMODO_OIDC_USE_FULL_EMAIL\n## Default: false.\noidc_use_full_email = false\n\n## Some providers attach other audiences in addition to the client_id.\n## If you have this issue, `Invalid audiences: `...` is not a trusted audience\"`,\n## you can add the audience `...` to the list here (assuming it should be trusted).\n## Env: KOMODO_OIDC_ADDITIONAL_AUDIENCES or KOMODO_OIDC_ADDITIONAL_AUDIENCES_FILE\n## Default: empty\noidc_additional_audiences = []\n\n#########\n# OAUTH #\n#########\n\n## Google\n\n## Env: KOMODO_GOOGLE_OAUTH_ENABLED\n## Default: false\ngoogle_oauth.enabled = false\n\n## Env: KOMODO_GOOGLE_OAUTH_ID or KOMODO_GOOGLE_OAUTH_ID_FILE\n## Required if google_oauth is enabled.\ngoogle_oauth.id = \"\"\n\n## Env: KOMODO_GOOGLE_OAUTH_SECRET or KOMODO_GOOGLE_OAUTH_SECRET_FILE\n## Required if google_oauth is enabled.\ngoogle_oauth.secret = \"\"\n\n## Github\n\n## Env: KOMODO_GITHUB_OAUTH_ENABLED\n## Default: false\ngithub_oauth.enabled = false\n\n## Env: KOMODO_GITHUB_OAUTH_ID or KOMODO_GITHUB_OAUTH_ID_FILE\n## Required if github_oauth is enabled.\ngithub_oauth.id = \"\"\n\n## Env: KOMODO_GITHUB_OAUTH_SECRET or KOMODO_GITHUB_OAUTH_SECRET_FILE\n## Required if github_oauth is enabled.\ngithub_oauth.secret = \"\"\n\n##################\n# POLL INTERVALS #\n##################\n\n## Controls the rate at which servers are polled for health, system stats, and container status.\n## This affects network usage, and the size of the stats stored in mongo.\n## Env: KOMODO_MONITORING_INTERVAL\n## Options: https://docs.rs/komodo_client/latest/komodo_client/entities/enum.Timelength.html\n## Default: 15-sec\nmonitoring_interval = \"15-sec\"\n\n## Interval at which to poll Resources for any updates / automated actions.\n## Env: KOMODO_RESOURCE_POLL_INTERVAL\n## Options: https://docs.rs/komodo_client/latest/komodo_client/entities/enum.Timelength.html\n## Default: 1-hr\nresource_poll_interval = \"1-hr\"\n\n############\n# Security #\n############\n\n## Enable HTTPS server using the given key and cert.\n## Env: KOMODO_SSL_ENABLED\n## Default: false\nssl_enabled = false\n\n## Path to the ssl key.\n## Env: KOMODO_SSL_KEY_FILE\n## Default: /config/ssl/key.pem\nssl_key_file = \"/config/ssl/key.pem\"\n\n## Path to the ssl cert.\n## Env: KOMODO_SSL_CERT_FILE\n## Default: /config/ssl/cert.pem\nssl_cert_file = \"/config/ssl/cert.pem\"\n\n############\n# DATABASE #\n############\n\n## Configure the database connection in one of the following ways:\n\n## Pass a full Mongo URI to the database.\n## Example: mongodb://username:password@localhost:27017\n## Env: KOMODO_DATABASE_URI or KOMODO_DATABASE_URI_FILE\n## Optional, can usually use `address`, `username`, `password` instead.\ndatabase.uri = \"\"\n\n## ==== * OR * ==== ##\n\n# Construct the address as mongodb://{username}:{password}@{address}\n## Env: KOMODO_DATABASE_ADDRESS\ndatabase.address = \"localhost:27017\"\n## Env: KOMODO_DATABASE_USERNAME or KOMODO_DATABASE_USERNAME_FILE\ndatabase.username = \"\"\n## Env: KOMODO_DATABASE_PASSWORD or KOMODO_DATABASE_PASSWORD_FILE\ndatabase.password = \"\"\n\n## ==== other ====\n\n## Komodo will create its collections under this database name.\n## The only reason to change this is if multiple Komodo Cores share the same db.\n## Env: KOMODO_DATABASE_DB_NAME\n## Default: komodo.\ndatabase.db_name = \"komodo\"\n\n## This is the assigned app_name of the mongo client.\n## The only reason to change this is if multiple Komodo Cores share the same db.\n## Env: KOMODO_DATABASE_APP_NAME\n## Default: komodo_core.\ndatabase.app_name = \"komodo_core\"\n\n############\n# WEBHOOKS #\n############\n\n## This token must be given to git provider during repo webhook config.\n## The secret configured on the git provider side must match the secret configured here.\n## If not provided, \n## Env: KOMODO_WEBHOOK_SECRET or KOMODO_WEBHOOK_SECRET_FILE\n## Optional, no default.\nwebhook_secret = \"a_random_webhook_secret\"\n\n## An alternate base url that is used to recieve git webhook requests.\n## If empty or not specified, will use 'host' address as base.\n## This is useful if Komodo is on an internal network, but can have a\n## proxy just allowing through the webhook listener api using NGINX.\n## Env: KOMODO_WEBHOOK_BASE_URL\n## Default: empty (none)\nwebhook_base_url = \"\"\n\n## Configure Github webhook app. Enables webhook management apis.\n## <INSERT LINK TO GUIDE>\n## Env: KOMODO_GITHUB_WEBHOOK_APP_APP_ID or KOMODO_GITHUB_WEBHOOK_APP_APP_ID_FILE\n# github_webhook_app.app_id = 1234455 # Find on the app page.\n## Env: \n##   - KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_IDS or KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_IDS_FILE\n##   - KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_NAMESPACES\n# github_webhook_app.installations = [\n#   ## Find the id after installing the app to user / organization. \"namespace\" is the username / organization name.\n#   { id = 1234, namespace = \"mbecker20\" }\n# ]\n\n## The path to Github webhook app private key. <INSERT LINK TO GUIDE>\n## This is defaulted to `/github/private-key.pem`, and doesn't need to be changed if running core in Docker.\n## Just mount the private key pem file on the host to `/github/private-key.pem` in the container.\n## Eg. `/your/path/to/key.pem : /github/private-key.pem`\n## Env: KOMODO_GITHUB_WEBHOOK_APP_PK_PATH\n# github_webhook_app.pk_path = \"/path/to/pk.pem\"\n\n###########\n# LOGGING #\n###########\n\n## Specify the logging verbosity\n## Env: KOMODO_LOGGING_LEVEL\n## Options: off, error, warn, info, debug, trace\n## Default: info\nlogging.level = \"info\"\n\n## Specify the logging format.\n## Env: KOMODO_LOGGING_STDIO\n## Options: standard, json, none\n## Default: standard\nlogging.stdio = \"standard\"\n\n## Optionally specify a opentelemetry otlp endpoint to send traces to.\n## Example: http://localhost:4317\n## Env: KOMODO_LOGGING_OTLP_ENDPOINT\nlogging.otlp_endpoint = \"\"\n\n## Set the opentelemetry service name.\n## This will be attached to the telemetry Komodo will send.\n## Env: KOMODO_LOGGING_OPENTELEMETRY_SERVICE_NAME\n## Default: \"Komodo\"\nlogging.opentelemetry_service_name = \"Komodo\"\n\n## Specify whether logging is more human readable.\n## Note. Single logs will span multiple lines.\n## Env: KOMODO_LOGGING_PRETTY\n## Default: false\nlogging.pretty = false\n\n## Specify whether startup config log\n## is more human readable (multi-line)\n## Env: KOMODO_PRETTY_STARTUP_CONFIG\n## Default: false\npretty_startup_config = false\n\n###########\n# PRUNING #\n###########\n\n## The number of days to keep historical system stats around, or 0 to disable pruning. \n## Stats older that are than this number of days are deleted on a daily cycle.\n## Env: KOMODO_KEEP_STATS_FOR_DAYS\n## Default: 14\nkeep_stats_for_days = 14\n\n## The number of days to keep alerts around, or 0 to disable pruning. \n## Alerts older that are than this number of days are deleted on a daily cycle.\n## Env: KOMODO_KEEP_ALERTS_FOR_DAYS\n## Default: 14\nkeep_alerts_for_days = 14\n\n###################\n# CLOUD PROVIDERS #\n###################\n\n## Komodo can build images by deploying AWS EC2 instances,\n## running the build, and afterwards destroying the instance.\n\n## Provide AWS api keys for ephemeral builders\n## Env: KOMODO_AWS_ACCESS_KEY_ID or KOMODO_AWS_ACCESS_KEY_ID_FILE\naws.access_key_id = \"\"\n## Env: KOMODO_AWS_SECRET_ACCESS_KEY or KOMODO_AWS_SECRET_ACCESS_KEY_FILE\naws.secret_access_key = \"\"\n\n#################\n# GIT PROVIDERS #\n#################\n\n## These will be available to attach to Builds, Repos, Stacks, and Syncs.\n## They allow these Resources to clone private repositories.\n## They cannot be configured on the environment.\n\n## configure git providers\n# [[git_provider]]\n# domain = \"github.com\"\n# accounts = [\n# \t{ username = \"mbecker20\", token = \"access_token_for_account\" },\n# \t{ username = \"moghtech\", token = \"access_token_for_other_account\" },\n# ]\n\n# [[git_provider]]\n# domain = \"git.mogh.tech\" # use a custom provider, like self-hosted gitea\n# accounts = [\n# \t{ username = \"mbecker20\", token = \"access_token_for_account\" },\n# ]\n\n# [[git_provider]]\n# domain = \"localhost:8000\" # use a custom provider, like self-hosted gitea\n# https = false # use http://localhost:8000 as base-url for clone\n# accounts = [\n# \t{ username = \"mbecker20\", token = \"access_token_for_account\" },\n# ]\n\n######################\n# REGISTRY PROVIDERS #\n######################\n\n## These will be available to attach to Builds and Stacks.\n## They allow these Resources to pull private images.\n## They cannot be configured on the environment.\n\n## configure docker registries\n# [[docker_registry]]\n# domain = \"docker.io\"\n# accounts = [\n# \t{ username = \"mbecker2020\", token = \"access_token_for_account\" }\n# ]\n# organizations = [\"DockerhubOrganization\"]\n\n# [[docker_registry]]\n# domain = \"git.mogh.tech\" # use a custom provider, like self-hosted gitea\n# accounts = [\n# \t{ username = \"mbecker20\", token = \"access_token_for_account\" },\n# ]\n# organizations = [\"Mogh\"] # These become available in the UI\n\n###########\n# SECRETS #\n###########\n\n## Provide Core based secrets.\n## These will be available to interpolate into your Deployment / Stack environments,\n## and will be hidden in the UI and logs.\n## These are available to use on any Periphery (Server),\n## but you can also limit access more by placing them in a single Periphery's config file instead.\n## These cannot be configured in the Komodo Core environment, they must be passed in the file.\n\n# [secrets]\n# SECRET_1 = \"value_1\"\n# SECRET_2 = \"value_2\""
  },
  {
    "path": "config/komodo.cli.toml",
    "content": "##########################\n# 🦎 KOMODO CLI CONFIG 🦎 #\n##########################\n\n## This is the offical \"Default\" config file for the Komodo CLI.\n## It serves as documentation for the meaning of the fields.\n## It is located at `https://github.com/moghtech/komodo/blob/main/config/komodo.cli.toml`.\n\n## Most fields can also be configured using cli arguments and environment variables.\n## These will will override values set in this file. (cli args > env > config files).\n\n## You can also use JSON or YAML if preferred. You can convert here:\n##   - YAML: https://it-tools.tech/toml-to-yaml\n##   - JSON: https://it-tools.tech/toml-to-json\n\n# Choose default profile to use with `km ...`\ndefault_profile = \"Default\"\n# default_profile = \"Alt\"\n\n# Set base values if they aren't defined in profile\n# Default: 14\nmax_backups = 7\n\n# Options: HorizontalOnly, VeriticalOnly, OutsideOnly, InsideOnly, AllBorders\n# Default: HorizontalOnly\ntable_format = \"HorizontalOnly\"\n\n[[profile]]\nname = \"Default\"\naliases = [\"d\"] # Use `km -p d ...`\n#\n# Env: KOMODO_CLI_HOST > KOMODO_HOST\nhost = \"https://komodo.example.com\"\n# Env: KOMODO_CLI_KEY\nkey = \"K-...\"\n# Env: KOMODO_CLI_SECRET\nsecret = \"S-...\"\n#\n# Env: KOMODO_CLI_BACKUPS_FOLDER\nbackups_folder = \"/backups\"\n# Env: KOMODO_CLI_MAX_BACKUPS\nmax_backups = 14\n#\n# DATABASE USED TO BACKUP / COPY FROM\n# \n## Env: KOMODO_DATABASE_URI or KOMODO_DATABASE_URI_FILE\ndatabase.uri = \"\"\n## ==== * OR * ==== ##\n# Construct the address as mongodb://{username}:{password}@{address}\n## Env: KOMODO_DATABASE_URI or KOMODO_DATABASE_URI_FILE\ndatabase.address = \"localhost:27017\"\n## Env: KOMODO_DATABASE_USERNAME or KOMODO_DATABASE_USERNAME_FILE\ndatabase.username = \"\"\n## Env: KOMODO_DATABASE_PASSWORD or KOMODO_DATABASE_PASSWORD_FILE\ndatabase.password = \"\"\n## Env: KOMODO_DATABASE_DB_NAME\n## Default: komodo.\ndatabase.db_name = \"komodo\"\n#\n# DATABASE USED TO RESTORE / COPY TO\n# \n## Env: KOMODO_CLI_DATABASE_TARGET_URI\ndatabase_target.uri = \"\"\n## ==== * OR * ==== ##\n# Construct the address as mongodb://{username}:{password}@{address}\n## Env: KOMODO_CLI_DATABASE_TARGET_URI\ndatabase_target.address = \"localhost:27017\"\n## Env: KOMODO_CLI_DATABASE_TARGET_USERNAME\ndatabase_target.username = \"\"\n## Env: KOMODO_CLI_DATABASE_TARGET_PASSWORD\ndatabase_target.password = \"\"\n## Env: KOMODO_CLI_DATABASE_TARGET_DB_NAME\n## Default: komodo.\ndatabase_target.db_name = \"komodo\"\n\n\n[[profile]]\nname = \"Alt\"\n# ... Configure same as above"
  },
  {
    "path": "config/periphery.config.toml",
    "content": "################################\n# 🦎 KOMODO PERIPHERY CONFIG 🦎 #\n################################\n\n## This is the offical \"Default\" config file for Komodo Periphery.\n## It serves as documentation for the meaning of the fields.\n## It is located at `https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml`.\n\n## All fields with a \"Default\" provided are optional. If they are\n## left out of the file, the \"Default\" value will be used.\n\n## If Periphery was installed on the host (systemd install script), this\n## file will be located either in `/etc/komodo/periphery.config.toml`,\n## or for user installs, `$HOME/.config/komodo/periphery.config.toml`.\n\n## Most fields can also be configured using environment variables.\n## Environment variables will override values set in this file.\n\n## You can also use JSON or YAML if preferred. You can convert here:\n##   - YAML: https://it-tools.tech/toml-to-yaml\n##   - JSON: https://it-tools.tech/toml-to-json\n\n## Optional. The port the server runs on.\n## Env: PERIPHERY_PORT\n## Default: 8120\nport = 8120\n\n## The IP address the periphery server will bind to.\n## The default will allow it to accept external IPv4 and IPv6 connections.\n## Env: PERIPHERY_BIND_IP\n## Default: [::]\nbind_ip = \"[::]\"\n\n## The directory periphery will use as the default base for the directories it uses.\n## The periphery user must have write access to this directory.\n## Each specific directory (like stack_dir) can be overridden below.\n## Env: PERIPHERY_ROOT_DIRECTORY\n## Default: /etc/komodo\nroot_directory = \"/etc/komodo\"\n\n## Optional. Override the directory periphery will use to manage repos.\n## The periphery user must have write access to this directory.\n## Env: PERIPHERY_REPO_DIR\n## Default: ${root_directory}/repos\n# repo_dir = \"/etc/komodo/repos\"\n\n## Optional. Override the directory periphery will use to manage stacks.\n## The periphery user must have write access to this directory.\n## Env: PERIPHERY_STACK_DIR\n## Default: ${root_directory}/stacks\n# stack_dir = \"/etc/komodo/stacks\"\n\n## Optional. Override the directory periphery will use to manage builds.\n## The periphery user must have write access to this directory.\n## Env: PERIPHERY_BUILD_DIR\n## Default: ${root_directory}/builds\n# build_dir = \"/etc/komodo/builds\"\n\n## Disable the terminal APIs and disallow remote shell access through Periphery.\n## Env: PERIPHERY_DISABLE_TERMINALS\n## Default: false\ndisable_terminals = false\n\n## Disable the container exec APIs and disallow remote container shell access through Periphery.\n## This can be left enabled while general terminal access is disabled.\n## Env: PERIPHERY_DISABLE_CONTAINER_EXEC\n## Default: false\ndisable_container_exec = false\n\n## How often Periphery polls the host for system stats, like CPU / memory usage.\n## To effectively disable polling, set this to something like 1-hr.\n## Env: PERIPHERY_STATS_POLLING_RATE\n## Options: https://docs.rs/komodo_client/latest/komodo_client/entities/enum.Timelength.html\n## Default: 5-sec\nstats_polling_rate = \"5-sec\"\n\n## How often Periphery polls the host for container stats,\n## Env: PERIPHERY_CONTAINER_STATS_POLLING_RATE\n## Options: https://docs.rs/komodo_client/latest/komodo_client/entities/enum.Timelength.html\n## Default: 30-sec\ncontainer_stats_polling_rate = \"30-sec\"\n\n## Whether stack actions should use `docker-compose ...`\n## instead of `docker compose ...`.\n## Env: PERIPHERY_LEGACY_COMPOSE_CLI\n## Default: false\nlegacy_compose_cli = false\n\n## Optional. Only include mounts at specific paths in the disk report.\n## Example: include_disk_mounts = [\"/mnt/include/1\", \"/mnt/include/2\"]\n## Env: PERIPHERY_INCLUDE_DISK_MOUNTS\n## Default: empty, which won't filter down the disks.\ninclude_disk_mounts = []\n\n## Optional. Don't include these mounts in the disk report.\n## Example: exclude_disk_mounts = [\"/mnt/exclude/1\", \"/mnt/exclude/2\"]\n## Env: PERIPHERY_EXCLUDE_DISK_MOUNTS\n## Default: empty, which won't exclude any disks.\nexclude_disk_mounts = []\n\n########\n# AUTH #\n########\n\n## Optional. Limit the ip addresses which can call the periphery api.\n## Supports Ipv4 / Ipv6 addresses and subnets.\n## Examples: allowed_ips = [\"::ffff:12.34.56.78\", \"10.0.10.0/24\"]\n## Env: PERIPHERY_ALLOWED_IPS\n## Default: empty, which will not block any request by ip.\nallowed_ips = []\n\n## Optional. Require callers to provide on of the provided passkeys to access the periphery api.\n## Example: passkeys = [\"your-passkey\"]\n## Env: PERIPHERY_PASSKEYS or PERIPHERY_PASSKEYS_FILE\n## Default: empty, which will not require any passkey to be passed by core.\npasskeys = []\n\n############\n# Security #\n############\n\n## Enable HTTPS server using the given key and cert.\n## If true and a key / cert at the given paths are not found, \n## self signed keys will be generated using openssl.\n## Env: PERIPHERY_SSL_ENABLED\n## Default: true\nssl_enabled = true\n\n## Path to the ssl key.\n## Env: PERIPHERY_SSL_KEY_FILE\n## Default: ${root_directory}/ssl/key.pem\n# ssl_key_file = \"/etc/komodo/ssl/key.pem\"\n\n## Path to the ssl cert.\n## Env: PERIPHERY_SSL_CERT_FILE\n## Default: ${root_directory}/ssl/cert.pem\n# ssl_cert_file = \"/etc/komodo/ssl/cert.pem\"\n\n###########\n# LOGGING #\n###########\n\n## Specify the logging verbosity\n## Options: off, error, warn, info, debug, trace\n## Default: info\n## Env: PERIPHERY_LOGGING_LEVEL\nlogging.level = \"info\"\n\n## Specify the logging format for stdout / stderr.\n## Env: PERIPHERY_LOGGING_STDIO\n## Options: standard, json, none\n## Default: standard\nlogging.stdio = \"standard\"\n\n## Specify a opentelemetry otlp endpoint to send traces to.\n## Example: http://localhost:4317.\n## Env: PERIPHERY_LOGGING_OTLP_ENDPOINT\n## Optional, no default\nlogging.otlp_endpoint = \"\"\n\n## Set the opentelemetry service name attached to the telemetry Periphery will send.\n## Env: PERIPHERY_LOGGING_OPENTELEMETRY_SERVICE_NAME\n## Default: \"Komodo\"\nlogging.opentelemetry_service_name = \"Periphery\"\n\n## Specify whether logging is more human readable.\n## Note. Single logs will span multiple lines.\n## Env: PERIPHERY_LOGGING_PRETTY\n## Default: false\nlogging.pretty = false\n\n## Specify whether startup config log\n## is more human readable (multi-line)\n## Env: PERIPHERY_PRETTY_STARTUP_CONFIG\n## Default: false\npretty_startup_config = false\n\n#################\n# GIT PROVIDERS #\n#################\n\n## configure Periphery based git providers\n# [[git_provider]]\n# domain = \"github.com\"\n# accounts = [\n# \t{ username = \"mbecker20\", token = \"access_token_for_account\" },\n# \t{ username = \"moghtech\", token = \"access_token_for_other_account\" },\n# ]\n\n# [[git_provider]]\n# domain = \"git.mogh.tech\" # use a custom provider, like self-hosted gitea\n# accounts = [\n# \t{ username = \"mbecker20\", token = \"access_token_for_account\" },\n# ]\n\n# [[git_provider]]\n# domain = \"localhost:8000\" # use a custom provider, like self-hosted gitea\n# https = false # use http://localhost:8000 as base-url for clone\n# accounts = [\n# \t{ username = \"mbecker20\", token = \"access_token_for_account\" },\n# ]\n\n######################\n# REGISTRY PROVIDERS #\n######################\n\n## Configure Periphery based docker registries\n# [[docker_registry]]\n# domain = \"docker.io\"\n# accounts = [\n# \t{ username = \"mbecker2020\", token = \"access_token_for_account\" }\n# ]\n# organizations = [\"DockerhubOrganization\"]\n\n# [[docker_registry]]\n# domain = \"git.mogh.tech\" # use a custom provider, like self-hosted gitea\n# accounts = [\n# \t{ username = \"mbecker20\", token = \"access_token_for_account\" },\n# ]\n# organizations = [\"Mogh\"] # These become available in the UI\n\n###########\n# SECRETS #\n###########\n\n## Provide periphery-based secrets\n# [secrets]\n# SECRET_1 = \"value_1\"\n# SECRET_2 = \"value_2\""
  },
  {
    "path": "deploy/deno.json",
    "content": "{}\n"
  },
  {
    "path": "deploy/komodo.ts",
    "content": "import * as TOML from \"jsr:@std/toml\";\n\nconst branch = await new Deno.Command(\"bash\", {\n  args: [\"-c\", \"git rev-parse --abbrev-ref HEAD\"],\n})\n  .output()\n  .then((r) => new TextDecoder(\"utf-8\").decode(r.stdout).trim());\n\nconst cargo_toml_str = await Deno.readTextFile(\"Cargo.toml\");\nconst prev_version = (\n  TOML.parse(cargo_toml_str) as {\n    workspace: { package: { version: string } };\n  }\n).workspace.package.version;\n\nconst [version, tag, count] = prev_version.split(\"-\");\nconst next_count = Number(count) + 1;\n\nconst next_version = `${version}-${tag}-${next_count}`;\n\nawait Deno.writeTextFile(\n  \"Cargo.toml\",\n  cargo_toml_str.replace(\n    `version = \"${prev_version}\"`,\n    `version = \"${next_version}\"`\n  )\n);\n\n// Cargo check first here to make sure lock file is updated before commit.\nconst cmd = `\ncargo check\necho \"\"\n\ngit add --all\ngit commit --all --message \"deploy ${version}-${tag}-${next_count}\"\n\necho \"\"\ngit push\necho \"\"\n\nkm run -y action deploy-komodo \"KOMODO_BRANCH=${branch}&KOMODO_VERSION=${version}&KOMODO_TAG=${tag}-${next_count}\"\n`\n  .split(\"\\n\")\n  .map((line) => line.trim())\n  .filter((line) => line.length > 0 && !line.startsWith(\"//\"))\n  .join(\" && \");\n\nnew Deno.Command(\"bash\", {\n  args: [\"-c\", cmd],\n}).spawn();\n"
  },
  {
    "path": "dev.compose.yaml",
    "content": "services:\n  core:\n    build:\n      context: .\n      dockerfile: bin/core/aio.Dockerfile\n    restart: unless-stopped\n    logging:\n      driver: local\n    networks:\n      - default\n    environment:\n      KOMODO_FIRST_SERVER: https://periphery:8120\n      KOMODO_DATABASE_ADDRESS: ferretdb\n      KOMODO_ENABLE_NEW_USERS: true\n      KOMODO_LOCAL_AUTH: true\n      KOMODO_JWT_SECRET: a_random_secret\n    volumes:\n      - repo-cache:/repo-cache\n\n  periphery:\n    build:\n      context: .\n      dockerfile: bin/periphery/aio.Dockerfile\n    restart: unless-stopped\n    logging:\n      driver: local\n    networks:\n      - default\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n      - /proc:/proc\n      - repos:/etc/komodo/repos\n      - stacks:/etc/komodo/stacks\n    environment:\n      PERIPHERY_INCLUDE_DISK_MOUNTS: /etc/hostname\n\n  ferretdb:\n    image: ghcr.io/ferretdb/ferretdb:1\n    restart: unless-stopped\n    logging:\n      driver: local\n    networks:\n      - default\n    environment:\n      - FERRETDB_HANDLER=sqlite\n    volumes:\n      - data:/state\n\nnetworks:\n  default: {}\n\nvolumes:\n  data:\n  repo-cache:\n  repos:\n  stacks:\n"
  },
  {
    "path": "docsite/.gitignore",
    "content": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "docsite/README.md",
    "content": "# Website\n\nThis website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.\n\n### Installation\n\n```\n$ yarn\n```\n\n### Local Development\n\n```\n$ yarn start\n```\n\nThis command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.\n\n### Build\n\n```\n$ yarn build\n```\n\nThis command generates static content into the `build` directory and can be served using any static contents hosting service.\n\n### Deployment\n\nUsing SSH:\n\n```\n$ USE_SSH=true yarn deploy\n```\n\nNot using SSH:\n\n```\n$ GIT_USER=<Your GitHub username> yarn deploy\n```\n\nIf you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.\n"
  },
  {
    "path": "docsite/babel.config.js",
    "content": "module.exports = {\n  presets: [require.resolve('@docusaurus/core/lib/babel/preset')],\n};\n"
  },
  {
    "path": "docsite/docs/ecosystem/api.md",
    "content": "# API and Clients\n\nKomodo Core exposes an RPC-like HTTP API to read data, write configuration, and execute actions.\nThere are typesafe clients available in\n[**Rust**](/docs/ecosystem/api#rust-client) and [**Typescript**](/docs/ecosystem/api#typescript-client).\n\nThe full API documentation is [**available here**](https://docs.rs/komodo_client/latest/komodo_client/api/index.html).\n\n## Rust Client\n\nThe Rust client is published to crates.io at [komodo_client](https://crates.io/crates/komodo_client).\n\n```rust\nlet komodo = KomodoClient::new(\"https://demo.komo.do\", \"your_key\", \"your_secret\")\n  .with_healthcheck()\n  .await?;\n\nlet stacks = komodo.read(ListStacks::default()).await?;\n\nlet update = komodo\n  .execute(DeployStack {\n    stack: stacks[0].name.clone(),\n    stop_time: None\n  })\n  .await?;\n```\n\n## Typescript Client\n\nThe Typescript client is published to NPM at [komodo_client](https://www.npmjs.com/package/komodo_client).\n\n```ts\nimport { KomodoClient, Types } from \"komodo_client\";\n\nconst komodo = KomodoClient(\"https://demo.komo.do\", {\n  type: \"api-key\",\n  params: {\n    key: \"your_key\",\n    secret: \"your secret\",\n  },\n});\n\n// Inferred as Types.StackListItem[]\nconst stacks = await komodo.read(\"ListStacks\", {});\n\n// Inferred as Types.Update\nconst update = await komodo.execute(\"DeployStack\", {\n  stack: stacks[0].name,\n});\n```\n"
  },
  {
    "path": "docsite/docs/ecosystem/cli.mdx",
    "content": "# Komodo CLI\n\nThe Komodo CLI, `km`, can be used to:\n  - Quickly **run executions** and update **resources** and **variables**.\n  - **Reset user passwords** and elevate users to **Super Admin**. \n  - Perform Database **backup**, **restore**, and **copy**.\n\nThe Komodo Core image comes packaged with the Komodo CLI,\nand is available for usage inside running container with `docker exec -it komodo-core km ...`.\nThis way, it inherits the Core database config in order to easily perform backups with `km db backup -y`.\n\n### Examples\n\n  - `km --help`\n  - `km deploy stack my-stack`\n  - `km run action my-action -y`\n  - `km database backup`\n  - `km db restore`\n  - `km set var MY_VAR my_value -y`\n  - `km update build my-build \"version=1.19.0&branch=release\"`\n  - `km x commit my-sync`\n  - `km set user mbecks super-admin true`\n  - `km set user mbecks password \"temp-password\"`\n\n### Install\n\nThere are binaries available for **Linux** (x86_64 / aarch64), **MacOS** (apple silicon), as\nwell as a distroless image: **`ghcr.io/moghtech/komodo-cli`**.\n\n#### Linux\n\nYou can install the binary using the following command:\n\nSystem-wide, as root, to `/usr/local/bin/km`:\n```bash\ncurl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/install-cli.py | python3\n```\n\nOr as non-root, to `${HOME}/.local/bin/km`:\n```bash\ncurl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/install-cli.py | python3 - --user\n```\n\n#### MacOS (Homebrew)\n\nAdd the `moghtech/komodo` tap, then install `km`:\n```bash\nbrew tap moghtech/komodo && \\\n  brew install km\n```\n\n#### Container\n\nYou can alias a docker run command:\n```bash\nalias km='docker run --rm -v $HOME/.config/komodo:/config ghcr.io/moghtech/komodo-cli km'\nkm config\n```\n\n### Configure\n\nThe CLI uses a configuration file to pass the Komodo host / api keys, database address and credentials,\nand configure some other behaviors. Additionally, all configuration fields can be individually overridden\nusing **CLI arguments** or **environment variables**, with CLI arguments having top priority.\n\nWhenever you want to check how config will be loaded, you can use the `km config` command\nto print it out.\n\n#### File detection\n\nWhen run, CLI will scan the **current working directory** in addition to `${HOME}/.config/komodo` for any files matching the wildcard pattern **`*komodo.cli*.*`**,\nparse them into a general representation, and then merge them together. Files which are detected later are merged later,\nmeaning they will override on conflicting fields. By default, files in `${HOME}/.config/komodo` come first\nin the merge ordering, meaning they are **lower priority** than those detected in the current working directory.\nYou can also override these default paths by passing `km -c /path/to/1/base.config.yaml -c ./overrides ...`.\n\nIf you want `km` to find configuration files in another directory,\nyou can make a `.kminclude` file inside one of the configured directories.\n\n```\n# Supports comments\n\n./.komodo    # relative to directory containing `.kminclude`\n\n/etc/komodo/komodo.cli.toml  # also supports absolute path\n```\n\nNote that wildcards in these paths are **not supported**.\n\n#### Profiles\n\nIn the files, you can configure multiple profiles, each with a name / aliases. Then you\nchoose which config profile to use with `km -p <profile> ...`. This allows you to easily switch between\nmultiple Cores you want to connect to, or different database backup / restore options.\n\nIn order to avoid passing `-p <profile>` every time, you can set a\n`default_profile` at the top level of the configuration file. Additionally,\nany fields you would like to be the \"default\" across all profiles can be \nset at the top level of the file.\n\n#### Example File\n\nThe configuration can also be passed as **YAML** or **JSON**.\nYou can use the it-tools to convert this TOML file to your preferred format:\n  - YAML: https://it-tools.tech/toml-to-yaml\n  - JSON: https://it-tools.tech/toml-to-json\n\nQuick download to `./komodo/komodo.cli.toml`:\n```bash\nwget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/config/komodo.cli.toml\n```\n\n```mdx-code-block\nimport RemoteCodeFile from \"@site/src/components/RemoteCodeFile\";\n\n<RemoteCodeFile\n\ttitle=\"https://github.com/moghtech/komodo/blob/main/config/komodo.cli.toml\"\n\turl=\"https://raw.githubusercontent.com/moghtech/komodo/main/config/komodo.cli.toml\"\n\tlanguage=\"toml\"\n/>\n```"
  },
  {
    "path": "docsite/docs/ecosystem/community.md",
    "content": "# Community\n\n### 3rd party tools\n- [Ansible Role Komodo](https://github.com/bpbradley/ansible-role-komodo) by [bpbradley](https://github.com/bpbradley)\n- [Komodo Import](https://foxxmd.github.io/komodo-import/docs/quickstart/) by [FoxxMD](https://github.com/FoxxMD)\n### Posts and Guides\n- [Migrating to Komodo](https://blog.foxxmd.dev/posts/migrating-to-komodo) by [FoxxMD](https://github.com/FoxxMD)\n- [FAQ, Tips, and Tricks](https://blog.foxxmd.dev/posts/komodo-tips-tricks) by [FoxxMD](https://github.com/FoxxMD)\n- [Compose Environments Explained](https://blog.foxxmd.dev/posts/compose-envs-explained) by [FoxxMD](https://github.com/FoxxMD)\n- [How To: Automate version updates for your self-hosted Docker containers with Gitea, Renovate, and Komodo](https://nickcunningh.am/blog/how-to-automate-version-updates-for-your-self-hosted-docker-containers-with-gitea-renovate-and-komodo) by [TheNickOfTime](https://github.com/TheNickOfTime)\n- [Setting up Komodo, comparison to Portainer, and FAQ](https://skyblog.one/komodo-the-better-alternative-to-portainer-for-container-management) by [Skyfay](https://skyblog.one/authors/)\n### Community Alerters\nThese provide alerting implementations which can be used with the `Custom` Alerter type.\n- [Discord](https://github.com/FoxxMD/deploy-discord-alerter) by [FoxxMD](https://github.com/FoxxMD)\n- [Telegram](https://github.com/mattsmallman/komodo-alert-to-telgram) by [mattsmallman](https://github.com/mattsmallman)\n- [Ntfy](https://github.com/FoxxMD/deploy-ntfy-alerter) by [FoxxMD](https://github.com/FoxxMD)\n- [Gotify](https://github.com/FoxxMD/deploy-gotify-alerter) by [FoxxMD](https://github.com/FoxxMD)\n- [Apprise](https://github.com/FoxxMD/deploy-apprise-alerter) by [FoxxMD](https://github.com/FoxxMD)\n- [Email](https://github.com/gutenye/email-notification/blob/main/src/templates/Komodo/Komodo.md) by [Guten Ye](https://github.com/gutenye)\n"
  },
  {
    "path": "docsite/docs/ecosystem/development.md",
    "content": "# Development\n\nIf you are looking to contribute to Komodo, this page is a launching point for setting up your Komodo development environment.\n\n## Dependencies\n\nRunning Komodo from [source](https://github.com/moghtech/komodo) requires either [Docker](https://www.docker.com/) (and can use the included [devcontainer](https://code.visualstudio.com/docs/devcontainers/containers)), or can have the development dependencies installed locally:\n\n* Backend (Core / Periphery APIs)\n    * [Rust](https://www.rust-lang.org/) stable via [rustup installer](https://rustup.rs/)\n    * [MongoDB](https://www.mongodb.com/) or [FerretDB](https://www.ferretdb.com/) available locally.\n    * On Debian/Ubuntu: `apt install build-essential pkg-config libssl-dev` required to build the rust source.\n* Frontend (Web UI)\n    * [Node](https://nodejs.org/en) >= 18.18 + NPM\n        * [Yarn](https://yarnpkg.com/) - (Tip: use `corepack enable` after installing `node` to use `yarn`)\n    * [typeshare](https://github.com/1password/typeshare)\n    * [Deno](https://deno.com/) >= 2.0.2\n\n### runnables-cli\n\n[mbecker20/runnables-cli](https://github.com/mbecker20/runnables-cli) can be used as a convience CLI for running common project tasks found in `runfile.toml`. Otherwise, you can create your own project tasks by references the `cmd`s found in `runfile.toml`. All instructions below will use runnables-cli v1.3.7+.\n\n## Docker\n\nAfter making changes to the project, run `run dev-compose-build` to rebuild Komodo and then `run dev-compose-exposed` to start a Komodo container with the UI accessible at `localhost:9120`. Any changes made to source files will require re-running the `dev-compose-build` and `dev-compose-exposed` commands.\n\n## Devcontainer\n\nUse the included `.devcontainer.json` with VSCode or other compatible IDE to stand-up a full environment, including database, with one click.\n\n[VSCode Tasks](https://code.visualstudio.com/Docs/editor/tasks) are provided for building and running Komodo. \n\nAfter opening the repository with the devcontainer run the task `Init` to build the frontend/backend. Then, the task `Run Komodo` can be used to run frontend/backend. Other tasks for rebuilding/running just one component of the stack (Core API, Periphery API, Frontend) are also provided.\n\n## Local\n\nTo run a full Komodo instance from a non-container environment run commands in this order:\n\n* Ensure dependencies are up to date\n    * `rustup update` -- ensure rust toolchain is up to date\n* Build and Run backend\n    * `run dev-core` -- Build and run Core API\n    * `run dev-periphery` -- Build and run Periphery API\n* Build Frontend\n    * Install **typeshare-cli**: `cargo install typeshare-cli`\n    * **Run this once** -- `run link-client` -- generates TS client and links to the frontend\n    * After running the above once:\n        * `run gen-client` -- Rebuild client \n        * `run dev-frontend` -- Start in dev (watch) mode\n        * `run build-frontend` -- Typecheck and build\n            \n\n## Docsite Development\n\nUse `run dev-docsite` to start the [Docusaurus](https://docusaurus.io/) Komodo docs site in development mode. Changes made to files in `./docsite` will be automatically reloaded by the server."
  },
  {
    "path": "docsite/docs/ecosystem/index.mdx",
    "content": "---\nslug: /ecosystem\n---\n\n# Ecosystem\n\n```mdx-code-block\nimport DocCardList from '@theme/DocCardList';\n\n<DocCardList />\n```"
  },
  {
    "path": "docsite/docs/intro.md",
    "content": "---\nslug: /intro\n---\n\n# What is Komodo?\n\nKomodo is a web app to provide structure for managing your servers, builds, deployments, and automated procedures.\n\nWith Komodo you can:\n\n - Connect all of your servers, alert on CPU usage, memory usage, and disk usage, and connect to shell sessions.\n - Create, start, stop, and restart Docker containers on the connected servers, view their status and logs, and connect to container shell.\n - Deploy docker compose stacks. The file can be defined in UI, or in a git repo, with auto deploy on git push.\n - Build application source into auto-versioned Docker images, auto built on webhook. Deploy single-use AWS instances for infinite capacity.\n - Manage repositories on connected servers, which can perform automation via scripting / webhooks.\n - Manage all your configuration / environment variables, with shared global variable and secret interpolation.\n - Keep a record of all the actions that are performed and by whom.\n\nThere is no limit to the number of servers you can connect, and there will never be. There is no limit to what API you can use for automation, and there never will be. No \"business edition\" here.\n\n## Docker\n\nKomodo is opinionated by design, and uses [docker](https://docs.docker.com/) as the container engine for building and deploying.\n\n:::info\nKomodo also supports [**podman**](https://podman.io/) instead of docker by utilizing the `podman` -> `docker` alias.\nFor Stack / docker compose support with podman, check out [**podman-compose**](https://github.com/containers/podman-compose). Thanks to `u/pup_kit` for checking this.\n:::\n\n## Architecture and Components\n\nKomodo is composed of a single core and any amount of connected servers running the periphery application. \n\n### Core\nKomodo Core is a web server hosting the Core API and browser UI. All user interaction with the connected servers flow through the Core.\n\n### Periphery\nKomodo Periphery is a small stateless web server that runs on all connected servers. It exposes an API called by Komodo Core to perform actions on the server, get system usage, and container status / logs. It is only intended to be reached from the core, and has an address whitelist to limit the IPs allowed to call this API.\n\n## Core API\n\nKomodo exposes powerful functionality over the Core's REST and Websocket API, enabling infrastructure engineers to manage their infrastructure programmatically. There is a [rust crate](https://crates.io/crates/komodo_client) and [npm package](https://www.npmjs.com/package/komodo_client) to simplify programmatic interaction with the API, but in general this can be accomplished using any programming language that can make REST requests. \n\n## Permissioning\n\nKomodo is a system designed to be used by many users, whether they are developers, operations personnel, or administrators. The ability to affect an applications state is very powerful, so Komodo has a granular permissioning system to only provide this functionality to the intended users. The permissioning system is explained in detail in the [permissioning](/docs/resources/permissioning) section. \n\nUser sign-on is possible using username / password, or with Oauth (Github and Google). See [Core Setup](./setup/index.mdx)."
  },
  {
    "path": "docsite/docs/resources/auto-update.md",
    "content": "# Automatic Updates\n\nStarting from **v1.19.0**, new Komodo installs will automatically create the\n**Global Auto Update** [Procedure](../resources/procedures#procedures), scheduled daily.\nIf you don't have it, this is the Toml:\n\n```toml\n[[procedure]]\nname = \"Global Auto Update\"\ndescription = \"Pulls and auto updates Stacks and Deployments using 'poll_for_updates' or 'auto_update'.\"\ntags = [\"system\"]\nconfig.schedule = \"Every day at 03:00\"\n\n[[procedure.config.stage]]\nname = \"Stage 1\"\nenabled = true\nexecutions = [\n  { execution.type = \"GlobalAutoUpdate\", execution.params = {}, enabled = true }\n]\n```\n\n:::info\nYou are also able to integrate `GlobalAutoUpdate` into other Procedures\nto coordinate the timing with other processes, such as backup. There is\nnothing special about this Procedure, it's just created by default for\nguidance / convenience.\n:::\n\n### How does it work?\n\nBoth Stacks and Deployments allow you to configure **Poll for Updates** or **Auto Update**.\nWhen [**GlobalAutoUpdate**](https://docs.rs/komodo_client/latest/komodo_client/api/execute/struct.GlobalAutoUpdate.html)\nis run, Komodo will loop through all the resources with either of these options enabled,\nand run [**PullStack**](https://docs.rs/komodo_client/latest/komodo_client/api/execute/struct.PullStack.html) / [**PullDeployment**](https://docs.rs/komodo_client/latest/komodo_client/api/execute/struct.PullDeployment.html)\nin order to pick up any newer images **at the same tag**.\nNote that in order to work, it requires use of a \"Rolling\" image tag, such as `:latest`.\n\n:::info\nIf you use git sources Stacks and want to automatically update image tags, check out\n[Renovate](https://github.com/renovatebot/renovate?tab=readme-ov-file#what-is-the-mend-renovate-cli)\n:::\n\nFor resources with **Poll for Updates** enabled and an Alerter configured, it will\nsend an alert that a newer image is available, and display the update available indicator in the UI\n\nFor resource with **Auto Update** enabled, it will go ahead and Redeploy *just the services* with\nnewer images (by default). If an Alerter is configured, it will also send an alert that this occured."
  },
  {
    "path": "docsite/docs/resources/build-images/builders.md",
    "content": "# Builders\n\nA builder is a machine running the Komodo Periphery agent (and usually docker), which is able to handle a RunBuild / BuildRepo command from Komodo core. Any server connected to Komodo can be chosen as the builder for a build.\n\nBuilding on a machine running production software is usually not a great idea, as this process can use a lot of system resources. It is better to start up a temporary cloud machine dedicated for the build, then shut it down when the build is finished. Komodo supports AWS EC2 for this task.\n\n## AWS builder\n\nBuilders are now Komodo resources, and are managed via the core API / can be updated using the UI.\nTo use this feature, you need an AWS EC2 AMI with docker and Komodo Periphery configured to run on system start.\nOnce you create your builder and add the necessary configuration, it will be available to attach to builds.\n\n### Setup the instance\n\nCreate an EC2 instance, and install Docker and Periphery on it.\n\nThe following script is an example of installing Docker and Periphery onto a Ubuntu/Debian instance:\n```sh\n#!/bin/bash\napt update\napt upgrade -y\ncurl -fsSL https://get.docker.com | sh\nsystemctl enable docker.service\nsystemctl enable containerd.service\ncurl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | HOME=/root python3\nsystemctl enable periphery.service\n```\n\n:::note\nAWS provides a \"user data\" feature, which will run a provided script as root. The above can be used with AWS user data\nto provide a hands free setup.\n:::\n\n### Make an AMI from the instance\n\nOnce the instance is up and running, ssh in and confirm Periphery is running using: \n\n```sh\nsudo systemctl status periphery.service\n```\n\nIf it is not, the install hasn't finished and you should wait a bit. It may take 5 minutes or more (all in updating / installing Docker, Periphery is just a 12 MB binary to download).\n\nOnce Periphery is running, you can navigate to the instance in the AWS UI and choose `Actions` -> `Image and templates` -> `Create image`. Just name the image and hit create.\n\nThe AMI will provide a unique id starting with `ami-`, use this with the builder configuration.\n\n### Configure security groups / firewall\nThe builders will need inbound access on port 8120 from Komodo Core, be sure to add a security group with this rule to the Builder configuration.\n\n## Multi-Platform Builds with Docker Buildx\n\nIf you need to build Docker images for multiple platforms (such as ARM and x86), Docker Buildx provides an easy way to do this.\n\n\n    Multi-platform builds can take significantly longer than single-platform builds.  \n    When emulating a different architecture (e.g., building ARM images on an x86 host), expect additional time due to QEMU-based emulation.\n\n\n### 1. Create and use a Buildx builder instance\n```sh\ndocker buildx create --name builder --use --bootstrap\n```\nThis command creates a new builder named `builder` and sets it as the active builder for the current Docker context.\n\n---\n\n### 2. Make Buildx the default for `docker build`\n```sh\ndocker buildx install\n```\nThis replaces the default `docker build` command with Buildx, so all builds automatically use the current builder instance.\n\n---\n\n### 3. (Optional) View available builders\n```sh\ndocker buildx ls\n```\nUse this to list all builder instances and check which one is active.\n\n---\n\nAfter these steps, any `docker build` command will use Buildx by default, making it straightforward to create multi-platform images.\n\n---\n\n### Platform selection in Komodo\nWhen building inside **Komodo**, you can specify the target platforms (e.g., `linux/amd64`, `linux/arm64`) directly in the Komodo UI during build configuration in the build \"Extra Args\" field.   \n\n**Example platform string for Extra Args:**\n```\n--platform linux/amd64,linux/arm64\n```"
  },
  {
    "path": "docsite/docs/resources/build-images/configuration.md",
    "content": "# Configuration\n\nKomodo just needs a bit of information in order to build your image.\n\n### Provider configuration\nKomodo supports cloning repos over http/s, from any provider that supports cloning private repos using `git clone https://<Token>@git-provider.net/<Owner>/<Repo>`.\n\nAccounts / access tokens can be configured in either the [core config](../../setup/advanced.mdx#mount-a-config-file)\nor in the [periphery config](../../setup/connect-servers.mdx#manual-install-steps---binaries).\n\n### Repo configuration\nTo specify the git repo to build, just give it the name of the repo and the branch under *repo config*. The name is given like `moghtech/komodo`, it includes the username / organization that owns the repo.\n\nMany repos are private, in this case an access token is needed by the building server.\nIt can either come from a provider defined in the core configuration,\nor in the periphery configuration of the building server.\n\n### Docker build configuration\n\nIn order to docker build, Komodo just needs to know the build directory and the path of the Dockerfile relative to the repo, you can configure these in the *build config* section.\n\nIf the build directory is the root of the repository, you pass the build path as `.`. If the build directory is some folder of the repo, just pass the name of the the folder. Do not pass the preceding \"/\". for example `build/directory`\n\nThe dockerfile's path is given relative to the build directory. So if your build directory is `build/directory` and the dockerfile is in `build/directory/Dockerfile.example`, you give the dockerfile path simply as `Dockerfile.example`.\n\n### Image registry\n\nKomodo supports pushing to any docker registry. \nAny of the accounts that are specified in config for the specific registry, between the core config and builder, will be available to use for authentication against the registry.\nAdditionally, allowed organizations on the docker registry can be specified on the core config and attached to builds.\nDoing so will cause the images to be published under the organization's namespace rather than the account's.\n\nWhen connecting a build to a deployments, the default behavior is for the deployment to inherit the registry configuration from the build.\nIn cases where that account isn't available to the deployment, another account can be chosen in the deployment config.\n\n:::note\nIn order to publish to the Github Container Registry, your Github access token must be given the `write:packages` permission.\nSee the Github docs [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic).\n:::\n\n### Adding build args\n\nThe Dockerfile may make use of [build args](https://docs.docker.com/engine/reference/builder/#arg). Build args can be passed using the gui by navigating to the `Build Args` tab in the config. They are passed in the menu just like in the would in a .env file:\n\n```\nBUILD_ARG1=some_value\nBUILD_ARG2=some_other_value\n```\n\nNote that these values are visible in the final image using `docker history`, so shouldn't be used to pass build time secrets. Use [secret mounts](https://docs.docker.com/engine/reference/builder/#run---mounttypesecret) for this instead.\n\n### Adding build secrets\n\nThe Dockerfile may also make use of [build secrets](https://docs.docker.com/build/building/secrets).\n\nThey are configured in the GUI the same way as build args. The values passed here can be used in RUN commands in the Dockerfile:\n```\nRUN --mount=type=secret,id=SECRET_KEY \\\n  SECRET_KEY=$(cat /run/secrets/SECRET_KEY) ...\n```\n\nThese values will not be visible with `docker history` command."
  },
  {
    "path": "docsite/docs/resources/build-images/index.mdx",
    "content": "---\nslug: /build-images\n---\n\n# Building Images\n\nKomodo builds docker images by cloning the source repository from the configured git provider, running `docker build`,\nand pushing the resulting image to the configured docker registry. Any repo containing a `Dockerfile` is buildable using this method.\n\n```mdx-code-block\nimport DocCardList from '@theme/DocCardList';\n\n<DocCardList />\n```"
  },
  {
    "path": "docsite/docs/resources/build-images/pre-build.md",
    "content": "# Pre-build command\n\nSometimes a command needs to be run before running ```docker build```, you can configure this in the *pre build* section. \n\nThere are two fields to pass for *pre build*. the first is *path*, which changes the working directory. To run the command in the root of the repo, just pass ```.```. The second field is *command*, this is the shell command to be executed after the repo is cloned.\n\nFor example, say your repo had a folder in it called ```scripts``` with a shell script ```on-clone.sh```. You would give *path* as ```scripts``` and command as ```sh on-clone.sh```. Or you could make *path* just ```.``` and then the command would be ```sh scripts/on-clone.sh```. Either way works fine."
  },
  {
    "path": "docsite/docs/resources/build-images/versioning.md",
    "content": "# Image Versioning\n\nKomodo uses a major.minor.patch versioning scheme to Build versioning. By default, every RunBuild will auto increment the Build's version patch number, and push the image to docker hub with the version tag, as well as the `latest` tag. A tag containing the latest short commit hash at the time the repo was cloned will also be created. \n\nYou can also turn off the auto incrementing feature, and manage the version yourself. In addition, you can configure a \"version tag\" on the build. This will postfix the version tag / commit hash tag with a custom label. For example, an image tag of `dev` will produce tags like `image_name:1.1.1-dev` and `image_name:h3c87c-dev`."
  },
  {
    "path": "docsite/docs/resources/deploy-containers/configuration.md",
    "content": "# Configuration\n\n## Choose the docker image\n\nThere are two options to configure the docker image to deploy. \n\n### Attaching a Komodo build\nIf the software you want to deploy is built by Komodo, you can attach the build directly to the deployment.\n\nBy default, Komodo will deploy the latest available version of the build, or you can specify a specific version using the version dropdown.\n\nAlso by default, Komodo will use the same docker account that is attached to the build in order to pull the image on the periphery server. If that account is not available on the server, you can specify another available account to use instead, this account just needs to have read access to the docker repository.\n\n### Using a custom image\nYou can also manually specify an image name, like `mongo` or `ghcr.io/mbecker20/random_image:0.1.1`.\n\nIf the image repository is private, you can still select an available docker account to use to pull the image.\n\n## Configuring the network\n\nOne feature of docker is that it allows for the creation of [virtual networks between containers](https://docs.docker.com/network/). Komodo allows you to specify a docker virtual network to connect the container to, or to use the host system networking to bypass the docker virtual network.\n\nThe default selection is `host`, which bypasses the docker virtual network layer.\n\nIf you do select select a network other than host, you can specify port bindings with the GUI. For example, if you are running mongo (which defaults to port 27017), you could use the mapping:\n\n```\n27018 : 27017\n```\n\nIn this case, you would access mongo from outside of the container on port `27018`.\n\nNote that this is not the only effect of using a network other than `host`. For example, containers running on different networks can not communicate, and ones on the same network can not reach other containers on `localhost` even when they are running on the same system. This behavior can be a bit confusing if you are not familiar with it, and it can be bypassed entirely by just using `host` network.\n\n## Configuring restart behavior\n\nDocker, like systemd, has a couple options for handling when a container exits. See [docker restart policies](https://docs.docker.com/config/containers/start-containers-automatically/). Komodo allows you to select the appropriate restart behavior from these options.\n\n## Configuring environment variables\n\nKomodo enables you to easily manage environment variables passed to the container. \nIn the GUI, navigate to the environment tab of the configuration on the deployment page.\n\nYou pass environment variables just as you would with a ```.env``` file:\n\n```\nENV_VAR_1=some_value\nENV_VAR_2=some_other_value\n```\n\n## Configuring volumes\n\nA docker container's filesystem is segregated from that of the host. However, it is still possible for a container to access system files and directories, this is accomplished by using [bind mounts](https://docs.docker.com/storage/bind-mounts/).\n\nSay your container needs to read a config file located on the system at ```/home/ubuntu/config.toml```. You can specify the bind mount to be:\n\n```\n/home/ubuntu/config.toml : /config/config.toml\n```\n\nThe first path is the one on the system, the second is the path in the container. Your application would then read the file at ```/config/config.toml``` in order to load its contents.\n\nThese can be configured easily with the GUI in the 'volumes' card. You can configure as many bind mounts as you need.\n\n## Extra args\n\nNot all features of docker are mapped directly by Komodo, only the most common. You can still specify any custom flags for Komodo to include in the `docker run` command by utilizing 'extra args'. For example, you can enable log rotation using these two extra args:\n\n```\n--log-opt max-size=10M\n```\n```\n--log-opt max-file=3\n```\n\n## Command\n\nSometimes you need to override the default command in the image, or specify some flags to be passed directly to the application. What is put here is inserted into the docker run command after the image. For example, to pass the `--quiet` flag to MongoDB, the docker run command would be:\n\n```\ndocker run -d --name mongo-db mongo:6.0.3 --quiet\n```\n\nIn order to achieve this with Komodo, just pass `--quiet` to 'command'.\n"
  },
  {
    "path": "docsite/docs/resources/deploy-containers/index.mdx",
    "content": "# Deploy Containers\n\nKomodo can deploy any docker images that it can access with the configured docker accounts.\nIt works by parsing the deployment configuration into a `docker run` command, which is then run on the target system.\nThe configuration is stored on MongoDB, and records of all actions (update config, deploy, stop, etc.) are stored as well.\n\n```mdx-code-block\nimport DocCardList from '@theme/DocCardList';\n\n<DocCardList />\n```\n"
  },
  {
    "path": "docsite/docs/resources/deploy-containers/lifetime-management.md",
    "content": "# Container Management\n\nThe lifetime of a docker container is more like a virtual machine. They can be created, started, stopped, and destroyed. Komodo will display the state of the container and provides an API to manage all your container's lifetimes.\n\nThis is achieved internally by running the appropriate docker command for the requested action (docker stop, docker start, etc).\n\n### Stopping a Container\n\nSometimes you want to stop a running application but preserve its logs and configuration, either to be restarted later or to view the logs at a later time. It is more like *pausing* the application with its current config, as no configuration (like environment variable, volume mounts, etc.) will be changed when the container is started again. \n\nNote that in order to restart an application with updated configuration, it must be *redeployed*. stopping and starting a container again will keep all configuration as it was when the container was initially created.\n\n### Container Redeploy\n\nRedeploying is the action of destroying a container and recreating it. If you update deployment config, these changes will not take effect until the container is redeployed. Just note this will destroy the previous containers logs along with the container itself."
  },
  {
    "path": "docsite/docs/resources/docker-compose.md",
    "content": "# Docker Compose\n\nKomodo can deploy docker compose projects through the `Stack` resource.\n\n## Define the compose file/s\n\nKomodo supports 3 ways of defining the compose files:\n\t1. **Write them in the UI**, and Komodo will write them to your host at deploy-time.\n\t2. **Store the files anywhere on the host**, and Komodo will just run the compose commands on the existing files.\n\t3. **Store them in a git repo**, and have Komodo clone it on the host to deploy.\n\nIf you manage your compose files in git repos:\n\n- All your files, across all servers, are available locally to edit in your favorite text editor.\n- All of your changes are tracked, and can be reverted.\n- You can use the git webhooks to do other automations when you change the compose file contents. Redeploying will be as easy as `git push`.\n\n:::info\nMany Komodo resources need access to git repos. There is an in-built token management system (managed in UI or in config file) to give resources access to credentials.\nAll resources which depend on git repos are able to use these credentials to access private repos.\n:::\n\n## Importing Existing Compose projects\n\nFirst create the Stack in Komodo, and ensure it has access to the compose files using one\nof the three methods above. Make sure to attach the server you wish to deploy on.\n\nIn order for Komodo to pick up a running project, it has to know the compose \"project name\".\nYou can find the project name by running `docker compose ls` on the host.\n\nBy default, Komodo will assume the Stack name is the compose project name.\nIf this is different than the project name on the host, you can configure a custom \"Project Name\" in the config.\n\n## Pass Environment Variables\n\nKomodo is able to pass custom environment variables to the docker compose process.\nThis works by:\n\n1. Write the variables to a \".env\" file on the host at deploy-time.\n2. Pass the file to docker compose using the `--env-file` flag.\n\n:::info\nJust like all other resources with Environments (Deployments, Repos, Builds),\nStack Environments support **Variable and Secret interpolation**. Define global variables\nin the UI and share the values across environments.\n:::"
  },
  {
    "path": "docsite/docs/resources/index.md",
    "content": "# Resources\n\nKomodo is extendible through the **Resource** abstraction. Entities like `Server`, `Deployment`, and `Stack` are all **Komodo Resources**.\n\nAll resources have common traits, such as a unique `name` and `id` amongst all other resources of the same resource type.\nAll resources can be assigned `tags`, which can be used to group related resources.\n\n:::note\nMany resources need access to git repos / docker registries. There is an in-built token management system (managed in UI or in config file) to give resources access to credentials.\nAll resources which depend on git repos / docker registries are able to use these credentials to access private repos.\n:::\n\n## [Server](setup/connect-servers)\n\n- Configure the connection to periphery agents.\n- Set alerting thresholds.\n- Can be attached to by **Deployments**, **Stacks**, **Repos**, and **Builders**.\n\n## [Deployment](resources/deploy-containers/index.mdx)\n\n- Deploy a docker container on the attached Server.\n- Manage services at the container level, perform orchestration using **Procedures** and **ResourceSyncs**.\n\n## [Stack](resources/docker-compose)\n\n- Deploy with docker compose.\n- Provide the compose file in UI, or move the files to a git repo and use a webhook for auto redeploy on push.\n- Supports composing multiple compose files using `docker compose -f ... -f ...`.\n- Pass environment variables usable within the compose file. Interpolate in app-wide variables / secrets.\n\n## Repo\n\n- Put scripts in git repos, and run them on a Server, or using a Builder.\n- Can build binaries, perform automation, really whatever you can think of.\n\n## [Build](resources/build-images/index.mdx)\n\n- Build application source into docker images, and push them to the configured registry.\n- The source can be any git repo containing a Dockerfile.\n\n## [Builder](resources/build-images/builders)\n\n- Either points to a connected server, or holds configuration to launch a single-use AWS instance to build the image.\n- Can be attached to **Builds** and **Repos**.\n\n## [Procedure](resources/procedures#procedures)\n\n- Compose many actions on other resource type, like `RunBuild` or `DeployStack`, and run it on button push (or with a webhook).\n- Can run one or more actions in parallel \"stages\", and compose a series of parallel stages to run sequentially.\n\n## [Action](resources/procedures#actions)\n\n- Write scripts calling the Komodo API in Typescript\n- Use a pre-initialized Komodo client within the script, no api keys necessary.\n- Type aware in UI editor. Get suggestions and see in depth docs as you type.\n- The Typescript client is also [published on NPM](https://www.npmjs.com/package/komodo_client).\n\n## [Resource Sync](resources/sync-resources)\n\n- Orchestrate all your configuration declaratively by defining it in `toml` files, which are checked into a git repo.\n- Can deploy **Deployments** and **Stacks** if changes are suggested.\n- Specify deploy ordering with `after` array. (like docker compose `depends_on` but can span across servers.).\n\n## Alerter\n\n- Route alerts to various endpoints.\n- Can configure rules on each Alerter, such as resource whitelist, blacklist, or alert type filter.\n"
  },
  {
    "path": "docsite/docs/resources/permissioning.md",
    "content": "# Permissioning\n\nKomodo has a granular, layer-based permissioning system to provide non-admin users access only to intended Resources.\n\n## User Groups\n\nWhile Komodo can assign permissions to specific users directly, it is recommended to instead **create User Groups and assign permissions to them**, as if they were a user.\n\nUsers can then be **added to multiple User Groups** and they **inherit the group's permissions**, similar to linux permissions.\nThere is also an `Everyone` mode for User Groups, if this is enabled then **all users implicitly gain the groups permissions**.\n\nFor permissioning at scale, users can define [**User Groups in Resource Syncs**](/docs/resources/sync-resources#user-group).\n\n## Permission Levels\n\nThere are 4 permission levels a user / group can be given on a Resource:\n\n 1. **None**. The user will not have any access to the resource. The user **will not see it in the GUI, and it will not show up if the user queries the Komodo API directly**. All attempts to view or update the resource will be blocked. This is the default for non-admins, unless using `KOMODO_TRANSPARENT_MODE=true`.\n\n 2. **Read**. This is the first permission level that grants any access. It will enable the user to **see the resource in the GUI and read the configuration**. Any attempts to update configuration or trigger any action **will be blocked**.  Using `KOMODO_TRANSPARENT_MODE=true` will make this level the base level on all resources, for all users.\n\n 3. **Execute**. This level will allow the user to execute actions on the resource, **like send a build command** or **trigger a redeploy**. The user will still be blocked from updating configuration on the resource.\n\n 4. **Write**. The user has full config write access to the resource, **they can execute any actions, update the configuration, and delete the resource**.\n\n## Specific Permissions\n\nPermission levels alone are not quite enough to provide granular access control.\nSome features are additionally gated behind a specific permission for that feature.\n\n- **`Logs`**: User can retrieve docker / docker compose logs on the associated resource.\n  - Valid on `Server`, `Stack`, `Deployment`.\n  - For admins wanting this permission by default for all users with read permissions, see below on default user groups.\n- **`Inspect`**:  User can \"inspect\" docker containers.\n  - Valid on `Server`, `Stack`, `Deployment`.\n  - **On Servers**: Access to this api will expose all container environments on the given server,\n  and can easily lead to secrets being leaked to unintended users if not protected.\n- **`Terminal`**: User can access the associated resource's terminal.\n  - If given on a `Server`, this allows server level terminal access, and all container exec priviledges (Including attached `Stacks` / `Deployments`).\n  - If given on a `Stack` or `Deployment`, this allows container exec terminal (even without `Terminal` on `Server`).\n- **`Attach`**: User can \"attach\" *other resources* to the resource.\n  - If given on a `Server`, allows users to attach `Stacks`, `Deployments`, `Repos`, and `Builders`.\n  - If given on a `Builder`, allows users to attach `Builds` and `Repos`.\n  - If given on a `Build`, allows users to attach it to `Deployments`.\n  - If given on a `Repo`, allows users to attach it to `Stacks`, `Builds`, and `Resource Syncs`.\n- **`Processes`**: User can retrieve the full running process list on the `Server`.\n\n## Permissioning by Resource Type\n\nUsers or User Groups can be given a base permission level on all Resources of a particular type, such as Stack.\nIn TOML form, this looks like:\n\n```toml\n[[user_group]]\nname = \"groupo\"\nusers = [\"mbecker20\", \"karamvirsingh98\"]\nall.Build = \"Execute\" # <- Group members can run all builds (but not update config),\nall.Stack = { level = \"Read\", specific = [\"Logs\"] }    # <- And see all Stacks / logs (no deploy / update, inspect, or terminal access).\n```\n\nA user / group can still be given a greater permission level on select resources:\n\n```toml\npermissions = [\n  # Grant addition specific permission (Logs are already granted above)\n  { target.type = \"Stack\", target.id = \"my-stack\", level = \"Execute\", specific = [\"Inspect\", \"Terminal\"] },\n  # Use regex to match multiple resources, for example give john execute on all of their Stacks\n  { target.type = \"Stack\", target.id = \"\\\\^john-(.+)$\\\\\", level = \"Execute\" },\n]\n```\n\n## Administration\n\nUsers can be given Admin privileges by a `Super Admin` (only the first user is given this status, set with `super_admin: true` on a User document in database). Super admins will see the \"Make Admin\" button when on a User page `/users/${user_id}`.\n\nThese users have unrestricted access to all Komodo Resources. Additionally, these users can update other (non-admin) user's permissions on resources.\n\nKomodo admins are responsible for managing user accounts as well. When a user logs into Komodo for the first time, they will not immediately be granted access (this can changed with `KOMODO_ENABLE_NEW_USERS=true`). An admin must first **enable** the user, which can be done from the `Users` tab on `Settings` page. Users can also be **disabled** by an admin at any time, which blocks all their access to the GUI and API. \n\nUsers also have some configurable global permissions, these are:\n\n - create server permission\n - create build permission\n\nOnly users with these permissions (as well as admins) can add additional servers to Komodo, and can create additional builds, respectively.\n"
  },
  {
    "path": "docsite/docs/resources/procedures.md",
    "content": "# Procedures and Actions\n\nFor orchestrations involving multiple resources and executions,\nKomodo offers the `Procedure` and `Action` resource types.\n\n## Procedures\n\n`Procedures` are compositions of many executions, such as `RunBuild` and `DeployStack`.\nThe executions are grouped into a series of `Stages`, where each `Stage` contains one or more executions\nto run **_all at once_**. The Procedure will wait until all of the executions in a `Stage` are complete before moving\non to the next stage. In short, the executions in a `Stage` are run **_in parallel_**, and the stages themselves are\nexecuted **_sequentially_**.\n\n### Batch Executions\n\nMany executions have a `Batch` version you can select, for example [**BatchDeployStackIfChanged**](https://docs.rs/komodo_client/latest/komodo_client/api/execute/struct.BatchDeployStackIfChanged.html). With this, you can match multiple Stacks by name\nusing [**wildcard syntax**](https://docs.rs/wildcard/latest/wildcard) and [**regex**](https://docs.rs/regex/latest/regex).\n\n### TOML Example\n\nLike all Resources, `Procedures` have a TOML representation, and can be managed in `ResourceSyncs`.\n\n```toml\n[[procedure]]\nname = \"pull-deploy\"\ndescription = \"Pulls stack-repo, deploys stacks\"\n\n[[procedure.config.stage]]\nname = \"Pull Repo\"\nexecutions = [\n  { execution.type = \"PullRepo\", execution.params.pattern = \"stack-repo\" },\n]\n\n[[procedure.config.stage]]\nname = \"Deploy if changed\"\nexecutions = [\n  # Uses the Batch version, witch matches many stacks by pattern\n  # This one matches all stacks prefixed with `foo-` (wildcard) and `bar-` (regex).\n  { execution.type = \"BatchDeployStackIfChanged\", execution.params.pattern = \"foo-* , \\\\^bar-.*$\\\\\" },\n]\n```\n\n## Actions\n\n`Actions` give users the power of Typescript to write calls to the Komodo API.\n\nFor example, an `Action` script like this will align the versions and branches of many `Builds`.\n\n```ts\nconst VERSION = \"1.16.5\";\nconst BRANCH = \"dev/\" + VERSION;\nconst APPS = [\"core\", \"periphery\"];\nconst ARCHS = [\"x86\", \"aarch64\"];\n\nawait komodo.write(\"UpdateVariableValue\", {\n  name: \"KOMODO_DEV_VERSION\",\n  value: VERSION,\n});\nconsole.log(\"Updated KOMODO_DEV_VERSION to \" + VERSION);\n\nfor (const app of APPS) {\n  for (const arch of ARCHS) {\n    const name = `komodo-${app}-${arch}-dev`;\n    await komodo.write(\"UpdateBuild\", {\n      id: name,\n      config: {\n        version: VERSION as any,\n        branch: BRANCH,\n      },\n    });\n    console.log(\n      `Updated Build ${name} to version ${VERSION} and branch ${BRANCH}`,\n    );\n  }\n}\n\nfor (const arch of ARCHS) {\n  const name = `periphery-bin-${arch}-dev`;\n  await komodo.write(\"UpdateRepo\", {\n    id: name,\n    config: {\n      branch: BRANCH,\n    },\n  });\n  console.log(`Updated Repo ${name} to branch ${BRANCH}`);\n}\n```"
  },
  {
    "path": "docsite/docs/resources/sync-resources.md",
    "content": "# Sync Resources\n\nKomodo is able to create, update, delete, and deploy resources declared in TOML files by diffing them against the existing resources,\nand apply updates based on the diffs. Similar to Stacks, the files can be configured in UI, in a local file, or in files pushed to a remote git repo.\nThe Komodo Core backend will poll the files for for any updates, and alert about pending changes when diffs are detected.\n\nYou can spread out your resource declarations across any number of files\nand use any nesting of folders to organize resources inside a root folder.\nAdditionally, you can create multiple `ResourceSyncs` and configure `Match Tags` to filter down which resources are synced,\nand each sync will be handled independently. This allows different syncs to manage resources on a \"per-project\" basis.\n\nThe UI will display the computed sync actions and only execute them upon manual confirmation.\nOr the sync execution git webhook may be configured on the git repo to\nautomatically execute syncs upon pushes to the configured branch.\n\n## Commit to Syncs\n\nIf the Sync is pointing to just a single file, you can enable \"Managed Mode\" to allow Core to write the updates you made in UI _back to the file_.\nThis works no matter where the files are located, and will create a commit to your git repository for repo based files.\n\n## Example Declarations\n\n### Server\n\n- [Server config schema](https://docs.rs/komodo_client/latest/komodo_client/entities/server/struct.ServerConfig.html)\n\n```toml\n[[server]] # Declare a new server\nname = \"server-prod\"\ndescription = \"the prod server\"\ntags = [\"prod\"]\n[server.config]\naddress = \"http://localhost:8120\"\nregion = \"AshburnDc1\"\nenabled = true # default: false\n```\n\n### Builder and build\n\n- [Builder config schema](https://docs.rs/komodo_client/latest/komodo_client/entities/builder/enum.BuilderConfig.html)\n- [Build config schema](https://docs.rs/komodo_client/latest/komodo_client/entities/build/struct.BuildConfig.html)\n\n```toml\n[[builder]] # Declare a builder\nname = \"builder-01\"\ntags = []\nconfig.type = \"Aws\"\n[builder.config.params]\nregion = \"us-east-2\"\nami_id = \"ami-0e9bd154667944680\"\n# These things come from your specific setup\nsubnet_id = \"subnet-xxxxxxxxxxxxxxxxxx\"\nkey_pair_name = \"xxxxxxxx\"\nassign_public_ip = true\nuse_public_ip = true\nsecurity_group_ids = [\n  \"sg-xxxxxxxxxxxxxxxxxx\",\n  \"sg-xxxxxxxxxxxxxxxxxx\"\n]\n\n##\n\n[[build]]\nname = \"test_logger\"\ndescription = \"Logs randomly at INFO, WARN, ERROR levels to test logging setups\"\ntags = [\"test\"]\n[build.config]\nbuilder_id = \"builder-01\"\nrepo = \"mbecker20/test_logger\"\nbranch = \"master\"\ngit_account = \"mbecker20\"\nimage_registry.type = \"Standard\"\nimage_registry.params.domain = \"github.com\" # or your custom domain\nimage_registry.params.account = \"your_username\"\nimage_registry.params.organization = \"your_organization\" # optinoal\n# Set docker labels\nlabels = \"\"\"\norg.opencontainers.image.source = https://github.com/mbecker20/test_logger\norg.opencontainers.image.description = Logs randomly at INFO, WARN, ERROR levels to test logging setups\norg.opencontainers.image.licenses = GPL-3.0\n\"\"\"\n```\n\n### Deployments\n\n- [Deployment config schema](https://docs.rs/komodo_client/latest/komodo_client/entities/deployment/struct.DeploymentConfig.html)\n\n```toml\n# Declare variables\n[[variable]]\nname = \"OTLP_ENDPOINT\"\nvalue = \"http://localhost:4317\"\n\n##\n\n[[deployment]] # Declare a deployment\nname = \"test-logger-01\"\ndescription = \"test logger deployment 1\"\ntags = [\"test\"]\n# sync will deploy the container:\n#  - if it is not running.\n#  - has relevant config updates.\n#  - the attached build has new version.\ndeploy = true\n[deployment.config]\nserver_id = \"server-01\"\nimage.type = \"Build\"\nimage.params.build = \"test_logger\"\n# set the volumes / bind mounts\nvolumes = \"\"\"\n# Supports comments\n/data/logs = /etc/logs\n# And other formats (eg yaml list)\n- \"/data/config:/etc/config\"\n\"\"\"\n# Set the environment variables\nenvironment = \"\"\"\n# Comments supported\nOTLP_ENDPOINT = [[OTLP_ENDPOINT]] # interpolate variables into the envs.\nVARIABLE_1 = value_1\nVARIABLE_2 = value_2\n\"\"\"\n# Set Docker labels\nlabels = \"deployment.type = logger\"\n\n##\n\n[[deployment]]\nname = \"test-logger-02\"\ndescription = \"test logger deployment 2\"\ntags = [\"test\"]\ndeploy = true\n# Create a dependency on test-logger-01. This deployment will only be deployed after test-logger-01 is deployed.\n# Additionally, any sync deploy of test-logger-01 will also trigger sync deploy of this deployment.\nafter = [\"test-logger-01\"]\n[deployment.config]\nserver_id = \"server-01\"\nimage.type = \"Build\"\nimage.params.build = \"test_logger\"\nvolumes = \"\"\"\n/data/logs = /etc/logs\n/data/config = /etc/config\"\"\"\nenvironment = \"\"\"\nVARIABLE_1 = value_1\nVARIABLE_2 = value_2\n\"\"\"\n# Set Docker labels\nlabels = \"deployment.type = logger\"\n```\n\n### Stack\n\n- [Stack config schema](https://docs.rs/komodo_client/latest/komodo_client/entities/stack/struct.StackConfig.html)\n\n```toml\n[[stack]]\nname = \"test-stack\"\ndescription = \"stack test\"\ndeploy = true\nafter = [\"test-logger-01\"] # Stacks can depend on deployments, and vice versa.\ntags = [\"test\"]\n[stack.config]\nserver_id = \"server-prod\"\nfile_paths = [\"mongo.yaml\", \"redis.yaml\"]\ngit_provider = \"git.mogh.tech\"\ngit_account = \"mbecker20\" # clone private repo by specifying account\nrepo = \"mbecker20/stack_test\"\n```\n\n### Procedure\n\n- [Procedure config schema](https://docs.rs/komodo_client/latest/komodo_client/entities/procedure/struct.ProcedureConfig.html)\n\n```toml\n[[procedure]]\nname = \"test-procedure\"\ndescription = \"Do some things in a specific order\"\ntags = [\"test\"]\n\n[[procedure.config.stage]]\nname = \"Build stuff\"\nexecutions = [\n  { execution.type = \"RunBuild\", execution.params.build = \"test_logger\" },\n  # Uses the Batch version, witch matches many builds by pattern\n  # This one matches all builds prefixed with `foo-` (wildcard) and `bar-` (regex).\n  { execution.type = \"BatchRunBuild\", execution.params.pattern = \"foo-* , \\\\^bar-.*$\\\\\" },\n  { execution.type = \"PullRepo\", execution.params.repo = \"komodo-periphery\" },\n]\n\n[[procedure.config.stage]]\nname = \"Deploy test logger 1\"\nexecutions = [\n  { execution.type = \"Deploy\", execution.params.deployment = \"test-logger-01\" },\n  { execution.type = \"Deploy\", execution.params.deployment = \"test-logger-03\", enabled = false },\n]\n\n[[procedure.config.stage]]\nname = \"Deploy test logger 2\"\nenabled = false\nexecutions = [\n  { execution.type = \"Deploy\", execution.params.deployment = \"test-logger-02\" }\n]\n```\n\n### Repo\n\n- [Repo config schema](https://docs.rs/komodo_client/latest/komodo_client/entities/repo/struct.RepoConfig.html)\n\n```toml\n[[repo]]\nname = \"komodo-periphery\"\ndescription = \"Builds new versions of the periphery binary. Requires Rust installed on the host.\"\ntags = [\"komodo\"]\n[repo.config]\nserver_id = \"server-01\"\ngit_provider = \"git.mogh.tech\" # use an alternate git provider (default is github.com)\ngit_account = \"mbecker20\"\nrepo = \"moghtech/komodo\"\n# Run an action after the repo is pulled\non_pull.path = \".\"\non_pull.command = \"\"\"\n# Supports comments\n/root/.cargo/bin/cargo build -p komodo_periphery --release\n# Multiple lines will be combined together using '&&'\ncp ./target/release/periphery /root/periphery\n\"\"\"\n```\n\n### Resource sync\n\n- [Resource sync config schema](https://docs.rs/komodo_client/latest/komodo_client/entities/sync/type.ResourceSync.html)\n\n```toml\n[[resource_sync]]\nname = \"resource-sync\"\n[resource_sync.config]\ngit_provider = \"git.mogh.tech\" # use an alternate git provider (default is github.com)\ngit_account = \"mbecker20\"\nrepo = \"moghtech/komodo\"\nresource_path = [\"stacks.toml\", \"repos.toml\"]\n```\n\n### User Group:\n\n- [UserGroup schema](https://docs.rs/komodo_client/latest/komodo_client/entities/toml/struct.UserGroupToml.html)\n\n```toml\n[[user_group]]\nname = \"groupo\"\neveryone = false # Set to true to give these permission to all users.\nusers = [\"mbecker20\", \"karamvirsingh98\"]\n# Configure write access with all specific permissions\nall.Server = { level = \"Write\", specific = [\"Attach\", \"Logs\", \"Inspect\", \"Terminal\", \"Processes\"] }\n# Attach base level of Execute on all builds\nall.Build = \"Execute\"\n# Allow users to see all Builders, and attach builds to them.\nall.Builder = { level = \"Read\", specific = [\"Attach\"] }\npermissions = [\n  # Attach permissions to specific resources by name\n  { target.type = \"Repo\", target.id = \"komodo-periphery\", level = \"Execute\" },\n  # Attach permissions to many resources with name matching regex (this uses '^(.+)-(.+)$' as regex expression)\n  { target.type = \"Server\", target.id = \"\\\\^(.+)-(.+)$\\\\\", level = \"Read\" },\n  { target.type = \"Deployment\", target.id = \"\\\\^immich\\\\\", level = \"Execute\" },\n]\n```\n"
  },
  {
    "path": "docsite/docs/resources/variables.md",
    "content": "# Variables and Secrets\n\nA variable / secret in Komodo is just a key-value pair.\n\n```\nKEY_1 = \"value_1\"\n```\n\nYou can interpolate the value into any resource Environment\n(and most other user configurable inputs, such as Repo `On Clone` and `On Pull`, or Stack `Extra Args`)\nusing double brackets around the key to trigger interpolation:\n\n```toml\n# Before interpolation\nSOME_ENV_VAR = [[KEY_1]] # <- wrap the key in double brackets '[[]]'\n\n# After iterpolation:\nSOME_ENV_VAR = value_1\n```\n\n## Defining Variables and Secrets\n\n- **In the UI**, you can go to `Settings` page, `Variables` tab. Here, you can create some Variables to store in the Komodo database.\n  - There is a \"secret\" option you can check, this will **prevent the value from exposure in any updates / logs**, as well as prevent access to the value to any **non-admin** Komodo users.\n  - Variables can also be managed in ResourceSyncs (see [example](/docs/resources/sync-resources#deployments)) but should only be done for non-secret variables, to avoid committing sensitive data. You should manage secrets using one of the following options.\n\n- **Mount a config file to Core**: https://komo.do/docs/setup/advanced#mount-a-config-file\n  - In the Komodo Core config file, you can configure `secrets` using a block like:\n\t\t```toml\n\t\t# in core.config.toml\n\t\t[secrets]\n\t\tKEY_1 = \"value_1\"\n\t\tKEY_2 = \"value_2\"\n\t\t```\n  - `KEY_1` and `KEY_2` will be available for interpolation on all your resources, as if they were Variables set up in the UI.\n  - They keys are queryable and show up on the variable page (so you know they are available for use),\n\t\tbut **the values are not exposed by API for ANY user**.\n\n- **Mount a config file to Periphery agent**:\n\n  - In the Komodo Periphery config file, you can also configure `secrets` using the same syntax as the Core config file.\n  - The variable **WILL NOT be available globally to all Komodo resources**, it will only be available to the resources on the associated Server resource on which that single Periphery agent is running.\n  - This effectively distributes your secret locations, can be good or bad depending on your security requirements. It does avoid the need to send the secret over network from Core to Periphery, Periphery based secrets are never exposed to the network.\n\n- **Use a dedicated secret management tool** such as Hashicorp Vault, alongside Komodo\n  - Ultimately Komodo variable / secret features **may not fill enterprise level secret management requirements**, organizations of this level should use still a dedicated secret management solution. At this point Komodo is not intended as an enterprise level secret management solution.\n  - These solutions do require application level integrations, your applications should only receive credentials to access the secret management API. **Your applications will pull the actual secret values from the dedicated secret management tool, they stay out of Komodo entirely**.\n"
  },
  {
    "path": "docsite/docs/resources/webhooks.md",
    "content": "# Configuring Webhooks\n\nMultiple Komodo resources can take advantage of webhooks from your git provider. Komodo supports incoming webhooks using either the Github or Gitlab webhook authentication type, which is also supported by other providers like Gitea.\n\n:::note\nOn Gitea, the default \"Gitea\" webhook type works with the Github authentication type 👍\n:::\n\n## Copy the Webhook URL\n\nFind the resource in UI, like a `Build`, `Repo`, or `Stack`.\nGo to the `Config` section, find \"Webhooks\", and copy the webhook for the action you want.\n\nThe webhook URL is constructed as follows:\n\n```shell\nhttps://${HOST}/listener/${AUTH_TYPE}/${RESOURCE_TYPE}/${ID_OR_NAME}/${EXECUTION}\n```\n- **`HOST`**: Your Komodo endpoint to recieve webhooks. \n\t- If your Komodo sits in a private network,\n\t  you will need a public proxy setup to forward `/listener` requests to Komodo.\n- **`AUTH_TYPE`**:\n\t- options: `github` | `gitlab`\n\t- `github`: Validates the signature attached with `X-Hub-Signature-256`. [reference](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries)\n\t- `gitlab`: Checks that the secret attached to `X-Gitlab-Token` is valid. [reference](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#create-a-webhook)\n- **`RESOURCE_TYPE`**:\n\t- options: `build` | `repo` | `stack` | `sync` | `procedure` | `action`\n- **`ID_OR_NAME`**:\n\t- Reference the specific resource by id or name. If the name may change, it is better to use id.\n- **`EXECUTION`**:\n\t- Which executions are available depends on the `RESOURCE_TYPE`. Builds only have the `/build` action.\n\t\tRepos can select between `/pull`, `/clone`, or `/build`. Stacks have `/deploy` and `/refresh`, and Resource Syncs have `/sync` and `/refresh`.\n\t- For **Procedures and Actions**, this will be the **branch to listen to for pushes**, or `__ANY__` to trigger\n\t\ton pushes to any branch.\n\n## Create the webhook on the Git Provider\n\nNavigate to the repo page on your git provider, and go to the settings for the Repo.\nFind Webhook settings, and click to create a new webhook.\n\nYou will have to input some information. \n\n1. The `Payload URL` is the link that you copied in the step above, `Copy the Resource Payload URL`.\n2. For Content-type, choose `application/json`\n3. For Secret, input the secret you configured in the Komodo Core config (`KOMODO_WEBHOOK_SECRET`).\n4. Enable SSL Verification, if you have proper TLS setup to your git provider (recommended).\n5. For \"events that trigger the webhook\", just the push request is what most people want.\n6. Of course, make sure the webhook is \"Active\" and hit create.\n\n## When does it trigger?\n\nYour git provider will now push this webhook to Komodo on *every* push to *any* branch. However, your `Build`, `Repo`,\netc. only cares about a specific branch of the repo.\n\nBecause of this, the webhook will trigger the action **only on pushes to the branch configured on the resource**.\n\nFor example, if I make a build, I may point the build to the `release` branch of a particular repo. If I set up a webhook, and push to the `main` branch, the action will *not trigger*. It will only trigger when the push is to the `release` branch.\n"
  },
  {
    "path": "docsite/docs/setup/advanced.mdx",
    "content": "# Advanced Configuration\n\n### OIDC / Oauth2\n\nTo enable OAuth2 login, you must create a client on the respective OAuth provider,\nfor example [Github](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)\nor [Google](https://developers.google.com/identity/protocols/oauth2).\n\nKomodo also supports self hosted Oauth2 providers like [Authentik](https://docs.goauthentik.io/docs/providers/oauth2/), [Gitea](https://docs.gitea.com/development/oauth2-provider) and [Keycloak](https://www.keycloak.org).\n\n- Komodo uses the `web application` login flow.\n- The redirect uri is:\n\t- `<KOMODO_HOST>/auth/github/callback` for Github.\n\t- `<KOMODO_HOST>/auth/google/callback` for Google.\n\t- `<KOMODO_HOST>/auth/oidc/callback` for OIDC.\n\n### Authentik\n\nCheck out the [Authentik official support documentation](https://integrations.goauthentik.io/infrastructure/komodo/).\n\n### Keycloak\n- Create an [OIDC client](https://www.keycloak.org/docs/latest/server_admin/index.html#proc-creating-oidc-client_server_administration_guide) in Keycloak.\n  - Note down the `Client ID` that you enter (e.g.: \"komodo\"), you will need it for Komodo configuration\n  - `Valid Redirect URIs`: use `<KOMODO_HOST>/auth/oidc/callback` and substitute `<KOMODO_HOST>` with your Komodo url.\n  - Turn `Client authentication` to `On`.\n  - After you finished creating the client, open it and go to `Credentials` tab and copy the `Client Secret`\n- Edit your environment variables for komodo core docker container and set the following:\n  - `KOMODO_OIDC_ENABLED=true`\n  - `KOMODO_OIDC_PROVIDER=https://<your Keycloak url>/realms/master` or replace `master` with another realm if you don't want to use the default one\n  - `KOMODO_OIDC_CLIENT_ID=...` what you specified as `Client ID`\n  - `KOMODO_OIDC_CLIENT_SECRET=...` that you copied from Keycloak\n\n\n### Mount a config file\n\nIf you prefer to keep sensitive information out of environment variables, you can optionally\nwrite a config file on your host, and mount it to `/config/config.toml` in the Komodo core container.\n\nThe configuration can also be passed as **YAML** or **JSON**.\nYou can use the it-tools to convert this TOML file to your preferred format:\n  - YAML: https://it-tools.tech/toml-to-yaml\n  - JSON: https://it-tools.tech/toml-to-json\n\n:::info\nConfiguration can still be passed in environment variables, and will take precedent over what is passed in the file.\n:::\n\nQuick download to `./komodo/core.config.toml`:\n```bash\nwget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/config/core.config.toml\n```\n\n```mdx-code-block\nimport RemoteCodeFile from \"@site/src/components/RemoteCodeFile\";\n\n<RemoteCodeFile\n\ttitle=\"https://github.com/moghtech/komodo/blob/main/config/core.config.toml\"\n\turl=\"https://raw.githubusercontent.com/moghtech/komodo/main/config/core.config.toml\"\n\tlanguage=\"toml\"\n/>\n```\n"
  },
  {
    "path": "docsite/docs/setup/backup.md",
    "content": "# Backup and Restore\n\n:::info\nDatabase backup and restore is actually a function of the [Komodo CLI](../ecosystem/cli),\nwhich is packaged in with the Komodo Core image for convenience.\n:::\n\nStarting from **v1.19.0**, new Komodo installs will automatically create the\n**Backup Core Database** [Procedure](../resources/procedures#procedures), scheduled daily.\nIf you don't have it, this is the Toml:\n\n```toml\n[[procedure]]\nname = \"Backup Core Database\"\ndescription = \"Triggers the Core database backup at the scheduled time.\"\ntags = [\"system\"]\nconfig.schedule = \"Every day at 01:00\"\n\n[[procedure.config.stage]]\nname = \"Stage 1\"\nenabled = true\nexecutions = [\n  { execution.type = \"BackupCoreDatabase\", execution.params = {}, enabled = true }\n]\n```\n\n:::info\nYou are also able to integrate `BackupCoreDatabase` into other Procedures, for example to trigger\nthis process before launching a backup container. There is nothing special about this Procedure,\nit's just created by default for guidance / convenience.\n:::\n\n## Backups\n\nWhen Komodo takes a database backup, it creates a **folder named for the time the backup was taken**,\nand dumps the gzip-compressed documents to files in this folder. \nIn order to store the backups to disk, **mount a host path to `/backups`** in the Komodo Core container.\n\nDue to its larger size and relative unimportance, the `Stats` collection (containing historical server cpu / mem / disk usage)\nis not included in dated backups. Just latest Stats are maintained at the top level of the backup folder.\n\nIn order to prevent unbounded growth, the backup process implements a pruning feature which will ensure\nonly the most recent 14 backup folders are kept. To change this number, set `max_backups` (`KOMODO_CLI_MAX_BACKUPS`)\nin `core.config.toml`, `komodo.cli.toml`, or in the Core container environment.\n\n```\n# Folder structure\n/backups\n| 2025-08-12_03-00-01\n| | Action.gz\n| | Alerter.gz\n| | ...\n| 2025-08-13_03-00-01\n| 2025-08-14_03-00-01\n| ...\n| Stats.gz\n```\n\n:::warning\nCurrently no encryption is supported,\nso you may want to encrypt the files before backing up remotely if your backup solution doesn't support that natively.\n:::\n\n## Remote Backups\n\nSince database backup is actually a function of the [Komodo CLI](../ecosystem/cli), you can also backup directly to\na remote server using the `ghcr.io/moghtech/komodo-cli` image. This service will backup once and then exit, so the scheduled deployment should still happen using a Procedure or Action:\n\n```yaml\nservices:\n  cli:\n    image: ghcr.io/moghtech/komodo-cli\n    command: km database backup -y\n    volumes:\n      - /path/to/komodo/backups:/backups\n    environment:\n      ## Database port must be reachable.\n      KOMODO_DATABASE_ADDRESS: komodo.example.com:27017\n      KOMODO_DATABASE_USERNAME: <db username>\n      KOMODO_DATABASE_PASSWORD: <db password>\n      KOMODO_DATABASE_DB_NAME: komodo\n      KOMODO_CLI_MAX_BACKUPS: 30 # set to your preference\n```\n\n## Restore\n\nThe Komodo CLI handles database restores as well.\n\n```yaml\nservices:\n  cli:\n    image: ghcr.io/moghtech/komodo-cli\n    ## Optionally specify a specific folder with `--restore-folder`,\n    ## otherwise restores the most recent backup.\n    command: km database restore -y # --restore-folder 2025-08-14_03-00-01\n    volumes:\n      # Same mount to backup files as above\n      - /path/to/komodo/backups:/backups\n    environment:\n      ## Database port must be reachable.\n      ## Note the different env vars needed compared to backup.\n      ## This is to prevent any accidental restores.\n      KOMODO_CLI_DATABASE_TARGET_ADDRESS: komodo.example.com:27017\n      KOMODO_CLI_DATABASE_TARGET_USERNAME: <db username>\n      KOMODO_CLI_DATABASE_TARGET_PASSWORD: <db password>\n      KOMODO_CLI_DATABASE_TARGET_DB_NAME: komodo-restore\n```\n\n:::warning\nThe restore process can be run multiple times with same backup files, and won't create any extra copies.\nHOWEVER it will not \"clear\" the target database beforehand. If the restore database is already populated,\nthose old documents will also remain. You may want to drop / delete the target database\nbefore restoring to it in this case.\n:::\n\n## Consistency\n\nSo long as the backup process completes successfully, the files produces can always be restored\nno matter how active the Komodo instance is at the time of backup. However writes that happen during\nthe backup process, such as updates to the resource configuration, may or may not be included in the backup\ndepending on the timing.\n\nWhile it should be rare that this causes any kind of issue when it comes to restoring, if your\nKomodo undergoes a lot of usage at all hours and you are worried about consistency,\nyou could consider [locking](https://www.mongodb.com/docs/manual/reference/method/db.fsyncLock/#mongodb-method-db.fsyncLock)\nMongo before the backup. Just make sure to [unlock](https://www.mongodb.com/docs/manual/reference/method/db.fsyncUnlock/)\nthe database afterwards."
  },
  {
    "path": "docsite/docs/setup/connect-servers.mdx",
    "content": "# Connect More Servers\n\n```mdx-code-block\nimport RemoteCodeFile from \"@site/src/components/RemoteCodeFile\";\n```\n\nConnecting a server to Komodo has 2 steps:\n\n1.  Install the Periphery agent on the server (either binary or container).\n2.  Add the server to Komodo via the Core API / UI.\n\n## Install Periphery\n\nYou can install Periphery as a systemd managed process, run it as a [docker container](https://github.com/moghtech/komodo/pkgs/container/komodo-periphery), or do whatever you want with the binary.\n\n:::warning\nAllowing unintended access to the Periphery agent API is a security risk.\nEnsure to take appropriate measures to block access to the Periphery API, such as firewall rules on port `8120`.\nAdditionally, you can whitelist your Komodo Core IP address in the [Periphery config](https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml#L46),\nand configure it to [only accept requests including your Core passkey](https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml#L51).\n:::\n\n### Install the Periphery agent - systemd\n\nAs root user:\n```bash\ncurl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | python3\n```\n\nPeriphery can also be installed to run as the calling user, just note this comes with some additional configuration.\n\n```bash\ncurl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | python3 - --user\n```\n\nYou can find more information (and view the script) in the [readme](https://github.com/moghtech/komodo/tree/main/scripts).\n\n:::info\nThis script can be run multiple times without issue, and it won't change existing config after the first run. Just run it again after a Komodo version release, and it will update the periphery version.\n:::\n\n:::tip\nFor deployment to many servers, a tool like [Ansible](https://docs.ansible.com/) should be used.\nAn example of such a setup can be found here: https://github.com/bpbradley/ansible-role-komodo\n:::\n\n### Install the Periphery agent - container\n\nYou can use a docker compose file:\n\n```mdx-code-block\n<RemoteCodeFile\n\ttitle=\"https://github.com/moghtech/komodo/blob/main/compose/periphery.compose.yaml\"\n\turl=\"https://raw.githubusercontent.com/moghtech/komodo/main/compose/periphery.compose.yaml\"\n\tlanguage=\"yaml\"\n/>\n```\n\n### Manual install steps - binaries\n\n1.  Download the periphery binary from the latest [release](https://github.com/moghtech/komodo/releases).\n\n2.  Create and edit your config files, following the [config example](https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml).\n\n:::note\nSee the [periphery config docs](https://docs.rs/komodo_client/latest/komodo_client/entities/config/periphery/index.html)\nfor more information on configuring periphery.\n:::\n\n3.  Ensure that inbound connectivity is allowed on the port specified in periphery.config.toml (default 8120).\n\n4.  Install docker. See the [docker install docs](https://docs.docker.com/engine/install/).\n\n:::note\nEnsure that the user which periphery is run as has access to the docker group without sudo.\n:::\n\n5.  Start the periphery binary with your preferred process manager, like systemd.\n\n### Example periphery start command\n\n```\nperiphery \\\n\t--config-path /path/to/periphery.config.base.toml \\\n\t--config-path /other_path/to/override-periphery-config-directory \\\n\t--config-keyword periphery \\\n\t--config-keyword config \\\n\t--merge-nested-config true\n```\n\n:::info\nYou can run `periphery --help` to see the manual.\n:::\n\nWhen running periphery in docker, use [command](https://docs.docker.com/reference/compose-file/services/#command) to pass in additional arguments.\n```\ncommand: periphery --config-path /path/in/container/to/periphery.config.base.toml\n```\n\n### Passing config files\n\nEither file paths or directory paths can be passed to `--config-path` (alias: `-c`). By default, no paths will be used, meaning the configuration is entirely\nloaded via environment variables.\n\nWhen using directories, the file entries can be filtered by name with the `--config-keyword` argument, which can be passed multiple times to add more keywords.\nThese are each wildcard patterns to match file names.\nOnly config files with file names that contain a keyword will be merged, with files matching later defined keywords having higher priority on field conflicts.\nBy default, the only keyword is `*config*.*`. This matches files like `config.toml`, `periphery.config.yaml`, etc.\n\nWhen passing multiple config files, later --config-path given in the command will always override previous ones.\nDirectory config files are merged in alphabetical order by name, so `config_b.toml` will override `config_a.toml`.\n\nThere are two ways to merge config files.\nThe default behavior is to completely replace any base fields with whatever fields are present in the override config.\nSo if you pass `allowed_ips = []` in your override config, the final allowed_ips will be an empty list as well.\n\n`--merge-nested-config true` will merge config fields recursively and extend config array fields.\n\nFor example, with `--merge-nested-config true` you can specify an allowed ip in the base config, and another in the override config, they will both be present in the final config.\n\nSimilarly, you can specify a base docker / github account pair, and extend them with additional accounts in the override config.\n\n## Configuration\n\nThe configuration can also be passed as **YAML** or **JSON**.\nYou can use the it-tools to convert this TOML file to your preferred format:\n  - YAML: https://it-tools.tech/toml-to-yaml\n  - JSON: https://it-tools.tech/toml-to-json\n\nQuick download to `./komodo/periphery.config.toml`:\n```bash\nwget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/config/periphery.config.toml\n```\n\n```mdx-code-block\n<RemoteCodeFile\n\ttitle=\"https://github.com/moghtech/komodo/blob/main/config/periphery.config.toml\"\n\turl=\"https://raw.githubusercontent.com/moghtech/komodo/main/config/periphery.config.toml\"\n\tlanguage=\"toml\"\n/>\n```\n"
  },
  {
    "path": "docsite/docs/setup/ferretdb.mdx",
    "content": "# FerretDB\n\n:::info\n- If you setup Komodo using **Postgres** or **Sqlite** options prior to [**Komodo v1.18.0**](https://github.com/moghtech/komodo/releases/tag/v1.18.0), you are using **FerretDB v1**.\n- Komodo now uses **FerretDB v2**. For existing users, [**upgrading requires a migration**](https://github.com/moghtech/komodo/blob/main/bin/cli/docs/copy-database.md#ferretdb-v2-update-guide).\n:::\n\n[**FerretDB**](https://www.ferretdb.com) is a MongoDB-compatible database backed by [Postgres + DocumentDB extension](https://github.com/microsoft/documentdb).\nIt is a solid option with performance comparable to MongoDB, and can also be run on some systems which [do not support MongoDB](https://github.com/moghtech/komodo/issues/59).\n\n1. Copy `komodo/ferretdb.compose.yaml` and `komodo/compose.env` to your host:\n```bash\nwget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/ferretdb.compose.yaml && \\\n  wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/compose.env\n```\n2. Edit the variables in `komodo/compose.env`.\n3. Deploy using command:\n\n```bash\ndocker compose -p komodo -f komodo/ferretdb.compose.yaml --env-file komodo/compose.env up -d\n```\n\n```mdx-code-block\nimport ComposeAndEnv from \"@site/src/components/ComposeAndEnv\";\n\n<ComposeAndEnv file_name=\"ferretdb.compose.yaml\" />\n```"
  },
  {
    "path": "docsite/docs/setup/index.mdx",
    "content": "# Setup Komodo Core\n\nTo run Komodo, you will need Docker. See [the docker install docs](https://docs.docker.com/engine/install/).\n\n### Deploy with Docker Compose\n\n- [**Using MongoDB**](./mongo.mdx)\n- [**Using FerretDB** (Postgres)](./ferretdb.mdx)\n\n:::info\nSome systems [do not support running the latest MongoDB versions](https://github.com/moghtech/komodo/issues/59).\nUsers with these systems should use FerretDB instead.\n:::\n\n:::info\n**FerretDB v1** users:\nThere is an [**upgrade guide for FerretDB v2** available here](https://github.com/moghtech/komodo/blob/main/bin/cli/docs/copy-database.md#ferretdb-v2-update-guide).\n:::\n\n### First login\n\nCore should now be accessible on the specified port and navigating to `http://<address>:<port>` will display the login page.\nEnter your preferred admin username and password, and click **\"Sign Up\"**, _not_ \"Log In\", to create your admin user for Komodo.\nAny additional users to create accounts will be disabled by default, and must be enabled by an admin.\n\n### Https\n\nKomodo Core only supports http, so a reverse proxy like [caddy](https://caddyserver.com/) should be used for https.\n\n```mdx-code-block\nimport DocCardList from '@theme/DocCardList';\n\n<DocCardList />\n```\n"
  },
  {
    "path": "docsite/docs/setup/mongo.mdx",
    "content": "# MongoDB\n\n[**MongoDB**](https://www.mongodb.com) is the standard database for Komodo.\nKomodo Core communicates with the database using the MongoDB driver.\n\n1. Copy `komodo/mongo.compose.yaml` and `komodo/compose.env` to your host:\n```bash\nwget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/mongo.compose.yaml && \\\n  wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/compose.env\n```\n2. Edit the variables in `komodo/compose.env`.\n3. Deploy using command:\n\n```bash\ndocker compose -p komodo -f komodo/mongo.compose.yaml --env-file komodo/compose.env up -d\n```\n\n```mdx-code-block\nimport ComposeAndEnv from \"@site/src/components/ComposeAndEnv\";\n\n<ComposeAndEnv file_name=\"mongo.compose.yaml\" />\n```"
  },
  {
    "path": "docsite/docs/setup/version-upgrades.md",
    "content": "# Version Upgrades\n\nMost version upgrades only require a redeployment of the Core container after pulling the latest version, and are fully backward compatible with the periphery clients, which may be updated later on as convenient. This is the default, and will be the case unless specifically mentioned in the [version release notes](https://github.com/moghtech/komodo/releases).\n\nSome Core API upgrades may change behavior such as building / cloning, and require updating the Periphery binaries to match the Core version before this functionality can be restored. This will be specifically mentioned in the release notes."
  },
  {
    "path": "docsite/docusaurus.config.ts",
    "content": "import {themes as prismThemes} from 'prism-react-renderer';\nimport type {Config} from '@docusaurus/types';\nimport type * as Preset from '@docusaurus/preset-classic';\n\nimport dotenv from \"dotenv\"\ndotenv.config();\n\nconst config: Config = {\n  title: \"Komodo\",\n  tagline: \"Build and deployment system\",\n  favicon: \"img/favicon.ico\",\n\n  // Set the production url of your site here\n  url: \"https://komo.do\",\n  // Set the /<baseUrl>/ pathname under which your site is served\n  // For GitHub pages deployment, it is often '/<projectName>/'\n  // baseUrl: \"/komodo/\",\n  baseUrl: \"/\",\n\n  // GitHub pages deployment config.\n  // If you aren't using GitHub pages, you don't need these.\n  organizationName: \"moghtech\", // Usually your GitHub org/user name.\n  projectName: \"komodo\", // Usually your repo name.\n  trailingSlash: false,\n  deploymentBranch: \"gh-pages-docs\",\n\n  onBrokenLinks: \"throw\",\n  onBrokenMarkdownLinks: \"warn\",\n\n  // Even if you don't use internationalization, you can use this field to set\n  // useful metadata like html lang. For example, if your site is Chinese, you\n  // may want to replace \"en\" with \"zh-Hans\".\n  i18n: {\n    defaultLocale: \"en\",\n    locales: [\"en\"],\n  },\n\n  presets: [\n    [\n      \"classic\",\n      {\n        docs: {\n          sidebarPath: \"./sidebars.ts\",\n          editUrl: \"https://github.com/moghtech/komodo/tree/main/docsite\",\n        },\n        blog: {\n          showReadingTime: true,\n          editUrl: \"https://github.com/moghtech/komodo/tree/main/docsite\",\n        },\n        theme: {\n          customCss: \"./src/css/custom.css\",\n        },\n      } satisfies Preset.Options,\n    ],\n  ],\n\n  themeConfig: {\n    image: \"img/monitor-lizard.png\",\n    docs: {\n      sidebar: {\n        autoCollapseCategories: true,\n      },\n    },\n    navbar: {\n      title: \"Komodo\",\n      logo: {\n        alt: \"monitor lizard\",\n        src: \"img/komodo-512x512.png\",\n        width: \"34px\",\n      },\n      items: [\n        {\n          type: \"docSidebar\",\n          sidebarId: \"docs\",\n          position: \"left\",\n          label: \"docs\",\n        },\n        {\n          href: \"https://opencollective.com/komodo\",\n          label: \"Donate\",\n          position: \"right\",\n        },\n        {\n          href: \"https://docs.rs/komodo_client/latest/komodo_client\",\n          label: \"Docs.rs\",\n          position: \"right\",\n        },\n        {\n          href: \"https://github.com/moghtech/komodo\",\n          label: \"Github\",\n          position: \"right\",\n        },\n      ],\n    },\n    footer: {\n      style: \"dark\",\n      copyright: `Built with Docusaurus`,\n    },\n    prism: {\n      theme: prismThemes.github,\n      darkTheme: prismThemes.dracula,\n      additionalLanguages: [\"bash\", \"yaml\", \"toml\"],\n    },\n  } satisfies Preset.ThemeConfig,\n};\n\nexport default config;\n"
  },
  {
    "path": "docsite/package.json",
    "content": "{\n  \"name\": \"docsite\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"docusaurus\": \"docusaurus\",\n    \"start\": \"docusaurus start\",\n    \"build\": \"docusaurus build\",\n    \"swizzle\": \"docusaurus swizzle\",\n    \"deploy\": \"docusaurus deploy\",\n    \"clear\": \"docusaurus clear\",\n    \"serve\": \"docusaurus serve\",\n    \"write-translations\": \"docusaurus write-translations\",\n    \"write-heading-ids\": \"docusaurus write-heading-ids\",\n    \"typecheck\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@docusaurus/core\": \"^3.8.1\",\n    \"@docusaurus/preset-classic\": \"^3.8.1\",\n    \"@mdx-js/react\": \"^3.1.0\",\n    \"clsx\": \"^2.1.1\",\n    \"prism-react-renderer\": \"^2.4.1\",\n    \"react\": \"^19.1.1\",\n    \"react-dom\": \"^19.1.1\"\n  },\n  \"devDependencies\": {\n    \"@docusaurus/module-type-aliases\": \"^3.8.1\",\n    \"@docusaurus/tsconfig\": \"^3.8.1\",\n    \"@docusaurus/types\": \"^3.8.1\",\n    \"dotenv\": \"^17.2.1\",\n    \"typescript\": \"^5.9.2\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 3 chrome version\",\n      \"last 3 firefox version\",\n      \"last 5 safari version\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">=18.0\"\n  },\n  \"packageManager\": \"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e\"\n}\n"
  },
  {
    "path": "docsite/runfile.toml",
    "content": "[dev-docsite]\nalias = \"dd\"\ndescription = \"starts the documentation site (https://komo.do) in dev mode\"\ncmd = \"yarn && yarn start\"\n\n[publish-docsite]\ndescription = \"publishes the documentation site (https://komo.do) to github pages\"\ncmd = \"yarn && yarn deploy\""
  },
  {
    "path": "docsite/sidebars.ts",
    "content": "import type { SidebarsConfig } from \"@docusaurus/plugin-content-docs\";\n\n/**\n * Creating a sidebar enables you to:\n - create an ordered group of docs\n - render a sidebar for each doc of that group\n - provide next/previous navigation\n\n The sidebars can be generated from the filesystem, or explicitly defined here.\n\n Create as many sidebars as you want.\n */\nconst sidebars: SidebarsConfig = {\n  docs: [\n    \"intro\",\n    {\n      type: \"category\",\n      label: \"Setup\",\n      link: {\n        type: \"doc\",\n        id: \"setup/index\",\n      },\n      items: [\n        \"setup/mongo\",\n        \"setup/ferretdb\",\n        \"setup/connect-servers\",\n        \"setup/backup\",\n        \"setup/advanced\",\n        \"setup/version-upgrades\",\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Resources\",\n      link: {\n        type: \"doc\",\n        id: \"resources/index\",\n      },\n      items: [\n        {\n          type: \"category\",\n          label: \"Build Images\",\n          link: {\n            type: \"doc\",\n            id: \"resources/build-images/index\",\n          },\n          items: [\n            \"resources/build-images/configuration\",\n            \"resources/build-images/pre-build\",\n            \"resources/build-images/builders\",\n            \"resources/build-images/versioning\",\n          ],\n        },\n        {\n          type: \"category\",\n          label: \"Deploy Containers\",\n          link: {\n            type: \"doc\",\n            id: \"resources/deploy-containers/index\",\n          },\n          items: [\n            \"resources/deploy-containers/configuration\",\n            \"resources/deploy-containers/lifetime-management\",\n          ],\n        },\n        \"resources/docker-compose\",\n        \"resources/auto-update\",\n        \"resources/variables\",\n        \"resources/procedures\",\n        \"resources/sync-resources\",\n        \"resources/webhooks\",\n        \"resources/permissioning\",\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Ecosystem\",\n      link: {\n        type: \"doc\",\n        id: \"ecosystem/index\",\n      },\n      items: [\n        \"ecosystem/cli\",\n        \"ecosystem/api\",\n        \"ecosystem/community\",\n        \"ecosystem/development\",\n      ],\n    },\n  ],\n};\n\nexport default sidebars;\n"
  },
  {
    "path": "docsite/src/components/ComposeAndEnv.tsx",
    "content": "import React from \"react\";\nimport RemoteCodeFile from \"./RemoteCodeFile\";\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\nexport default function ComposeAndEnv({\n  file_name,\n}: {\n  file_name: string;\n}) {\n  return (\n    <Tabs>\n      <TabItem value={file_name}>\n        <RemoteCodeFile\n          title={`https://github.com/moghtech/komodo/blob/main/compose/${file_name}`}\n          url={`https://raw.githubusercontent.com/moghtech/komodo/main/compose/${file_name}`}\n          language=\"yaml\"\n        />\n      </TabItem>\n      <TabItem value=\"compose.env\">\n        <RemoteCodeFile\n          title=\"https://github.com/moghtech/komodo/blob/main/compose/compose.env\"\n          url=\"https://raw.githubusercontent.com/moghtech/komodo/main/compose/compose.env\"\n          language=\"bash\"\n        />\n      </TabItem>\n    </Tabs>\n  );\n}\n"
  },
  {
    "path": "docsite/src/components/Divider.tsx",
    "content": "import React from \"react\";\n\nexport default function Divider() {\n  return (\n    <div\n      style={{\n        opacity: 0.7,\n        backgroundColor: \"rgb(175, 175, 175)\",\n        height: \"3px\",\n        width: \"100%\",\n        margin: \"75px 0px\",\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "docsite/src/components/HomepageFeatures/index.tsx",
    "content": "import clsx from 'clsx';\nimport Heading from '@theme/Heading';\nimport styles from './styles.module.css';\n\ntype FeatureItem = {\n  title: string;\n  description: JSX.Element;\n};\n\nconst FeatureList: FeatureItem[] = [\n  {\n    title: \"Automated builds 🛠️\",\n    description: (\n      <>\n        Build auto versioned docker images from git repos, trigger builds on\n        git push\n      </>\n    ),\n  },\n  {\n    title: \"Deploy docker containers 🚀\",\n    description: (\n      <>\n        Deploy containers, deploy docker compose, see uptime and logs across all\n        your servers\n      </>\n    ),\n  },\n  {\n    title: \"Powered by Rust 🦀\",\n    description: <>The core API and periphery agent are written in Rust</>,\n  },\n];\n\nfunction Feature({title, description}: FeatureItem) {\n  return (\n    <div className={clsx('col col--4')}>\n      <div className=\"text--center padding-horiz--md\">\n        <Heading as=\"h3\">{title}</Heading>\n        <p>{description}</p>\n      </div>\n    </div>\n  );\n}\n\nexport default function HomepageFeatures(): JSX.Element {\n  return (\n    <section className={styles.features}>\n      <div className=\"container\">\n        <div className=\"row\">\n          {FeatureList.map((props, idx) => (\n            <Feature key={idx} {...props} />\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "docsite/src/components/HomepageFeatures/styles.module.css",
    "content": ".features {\n  display: flex;\n  align-items: center;\n  padding: 4rem 0;\n  width: 100%;\n}\n\n.featureSvg {\n  height: 200px;\n  width: 200px;\n}\n"
  },
  {
    "path": "docsite/src/components/KomodoLogo.tsx",
    "content": "import React from \"react\";\n\nexport default function KomodoLogo({ width = \"4rem\" }) {\n  return (\n    <img\n      style={{ width, height: \"auto\", opacity: 0.7 }}\n      src=\"img/monitor-lizard.png\"\n      alt=\"monitor-lizard\"\n    />\n  );\n}\n"
  },
  {
    "path": "docsite/src/components/RemoteCodeFile.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport CodeBlock from \"@theme/CodeBlock\";\n\nasync function fetch_text_set(url: string, set: (text: string) => void) {\n  const res = await fetch(url);\n  const text = await res.text();\n  set(text);\n}\n\nexport default function RemoteCodeFile({\n  url,\n  language,\n  title,\n}: {\n  url: string;\n  language?: string;\n  title?: string;\n}) {\n  const [file, setFile] = useState(\"\");\n  useEffect(() => {\n    fetch_text_set(url, setFile);\n  }, []);\n  return (\n    <CodeBlock title={title ?? url} language={language} showLineNumbers>\n      {file}\n    </CodeBlock>\n  );\n}\n"
  },
  {
    "path": "docsite/src/components/SummaryImg.tsx",
    "content": "import React from \"react\";\n\nexport default function SummaryImg() {\n\treturn (\n    <div style={{ display: \"flex\", justifyContent: \"center\" }}>\n      <img\n        style={{ marginBottom: \"4rem\", width: \"1000px\" }}\n        src=\"img/monitor-summary.png\"\n        alt=\"monitor-summary\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "docsite/src/css/custom.css",
    "content": "/**\n * Any CSS included here will be global. The classic template\n * bundles Infima by default. Infima is a CSS framework designed to\n * work well for content-centric websites.\n */\n\n/* You can override the default Infima variables here. */\n:root {\n  --ifm-color-primary: #2e8555;\n  --ifm-color-primary-dark: #29784c;\n  --ifm-color-primary-darker: #277148;\n  --ifm-color-primary-darkest: #205d3b;\n  --ifm-color-primary-light: #33925d;\n  --ifm-color-primary-lighter: #359962;\n  --ifm-color-primary-lightest: #3cad6e;\n  --ifm-code-font-size: 95%;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);\n}\n\n/* For readability concerns, you should choose a lighter palette in dark mode. */\n[data-theme='dark'] {\n  --ifm-color-primary: #25c2a0;\n  --ifm-color-primary-dark: #21af90;\n  --ifm-color-primary-darker: #1fa588;\n  --ifm-color-primary-darkest: #1a8870;\n  --ifm-color-primary-light: #29d5b0;\n  --ifm-color-primary-lighter: #32d8b4;\n  --ifm-color-primary-lightest: #4fddbf;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);\n}\n"
  },
  {
    "path": "docsite/src/pages/index.module.css",
    "content": "/**\n * CSS files with the .module.css suffix will be treated as CSS modules\n * and scoped locally.\n */\n\n.heroBanner {\n  padding: 4rem 0;\n  text-align: center;\n  position: relative;\n  overflow: hidden;\n}\n\n@media screen and (max-width: 996px) {\n  .heroBanner {\n    padding: 2rem;\n  }\n}\n\n.buttons {\n  display: grid;\n  gap: 1rem;\n  grid-template-columns: 1fr 1fr;\n  width: fit-content;\n}\n"
  },
  {
    "path": "docsite/src/pages/index.tsx",
    "content": "import clsx from \"clsx\";\nimport Link from \"@docusaurus/Link\";\nimport useDocusaurusContext from \"@docusaurus/useDocusaurusContext\";\nimport Layout from \"@theme/Layout\";\nimport HomepageFeatures from \"@site/src/components/HomepageFeatures\";\n\nimport styles from \"./index.module.css\";\nimport KomodoLogo from \"../components/KomodoLogo\";\n\nfunction HomepageHeader() {\n  const { siteConfig } = useDocusaurusContext();\n  return (\n    <header className={clsx(\"hero hero--primary\", styles.heroBanner)}>\n      <div className=\"container\">\n        <div style={{ display: \"flex\", gap: \"1rem\", justifyContent: \"center\" }}>\n          <div style={{ position: \"relative\" }}>\n            <KomodoLogo width=\"600px\" />\n            <h1\n              className=\"hero__title\"\n              style={{\n                margin: 0,\n                position: \"absolute\",\n                top: \"40%\",\n                left: \"50%\",\n                transform: \"translate(-50%, -50%)\",\n              }}\n            >\n              Komodo\n            </h1>\n          </div>\n        </div>\n        <p className=\"hero__subtitle\">{siteConfig.tagline}</p>\n        <div style={{ display: \"flex\", justifyContent: \"center\" }}>\n          <div className={styles.buttons}>\n            <Link\n              className=\"button button--secondary button--lg\"\n              to=\"/docs/intro\"\n            >\n              Docs\n            </Link>\n            <Link\n              className=\"button button--secondary button--lg\"\n              to=\"https://github.com/moghtech/komodo\"\n            >\n              Github\n            </Link>\n            <Link\n              className=\"button button--secondary button--lg\"\n              to=\"https://github.com/moghtech/komodo#screenshots\"\n              style={{\n                width: \"100%\",\n                boxSizing: \"border-box\",\n                gridColumn: \"span 2\",\n              }}\n            >\n              Screenshots\n            </Link>\n            <Link\n              className=\"button button--secondary button--lg\"\n              to=\"https://demo.komo.do\"\n              style={{\n                width: \"100%\",\n                boxSizing: \"border-box\",\n                gridColumn: \"span 2\",\n              }}\n            >\n              Demo\n            </Link>\n          </div>\n        </div>\n      </div>\n    </header>\n  );\n}\n\nexport default function Home(): JSX.Element {\n  const { siteConfig } = useDocusaurusContext();\n  return (\n    <Layout title=\"Home\" description={siteConfig.tagline}>\n      <HomepageHeader />\n      <main>\n        <HomepageFeatures />\n      </main>\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "docsite/static/.nojekyll",
    "content": ""
  },
  {
    "path": "docsite/tsconfig.json",
    "content": "{\n  // This file is not used in compilation. It is here just for a nice editor experience.\n  \"extends\": \"@docusaurus/tsconfig\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\"\n  }\n}\n"
  },
  {
    "path": "example/alerter/Cargo.toml",
    "content": "[package]\nname = \"alerter\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n# local\nkomodo_client.workspace = true\nlogger.workspace = true\n# external\ntokio.workspace = true\ntracing.workspace = true\naxum.workspace = true\nanyhow.workspace = true\nserde.workspace = true\ndotenvy.workspace = true\nenvy.workspace = true"
  },
  {
    "path": "example/alerter/Dockerfile",
    "content": "FROM rust:1.89.0 as builder\nWORKDIR /builder\n\nCOPY . .\n\nRUN cargo build -p alert_logger --release\n\nFROM gcr.io/distroless/debian-cc\n\nCOPY --from=builder /builder/target/release/alert_logger /\n\nEXPOSE 7000\n\nCMD [\"./alert_logger\"]"
  },
  {
    "path": "example/alerter/README.md",
    "content": "# Alerter\n\nThis crate sets up a basic axum server that listens for incoming alert POSTs.\nIt can be used as a Komodo alerting endpoint, and serves as a template for other custom alerter implementations."
  },
  {
    "path": "example/alerter/src/main.rs",
    "content": "#[macro_use]\nextern crate tracing;\n\nuse std::{net::SocketAddr, str::FromStr};\n\nuse anyhow::Context;\nuse axum::{routing::post, Json, Router};\nuse komodo_client::entities::alert::{Alert, SeverityLevel};\nuse serde::Deserialize;\n\n/// Entrypoint for handling each incoming alert.\nasync fn handle_incoming_alert(Json(alert): Json<Alert>) {\n  if alert.resolved {\n    info!(\"Alert Resolved!: {alert:?}\");\n    return;\n  }\n  match alert.level {\n    SeverityLevel::Ok => info!(\"{alert:?}\"),\n    SeverityLevel::Warning => warn!(\"{alert:?}\"),\n    SeverityLevel::Critical => error!(\"{alert:?}\"),\n  }\n}\n\n/// ========================\n/// Http server boilerplate.\n/// ========================\n\n#[derive(Deserialize)]\nstruct Env {\n  #[serde(default = \"default_port\")]\n  port: u16,\n}\n\nfn default_port() -> u16 {\n  7000\n}\n\nasync fn app() -> anyhow::Result<()> {\n  dotenvy::dotenv().ok();\n  logger::init(&Default::default())?;\n\n  let Env { port } =\n    envy::from_env().context(\"failed to parse env\")?;\n\n  let socket_addr = SocketAddr::from_str(&format!(\"0.0.0.0:{port}\"))\n    .context(\"invalid socket addr\")?;\n\n  info!(\"v {} | {socket_addr}\", env!(\"CARGO_PKG_VERSION\"));\n\n  let app = Router::new().route(\"/\", post(handle_incoming_alert));\n\n  let listener = tokio::net::TcpListener::bind(socket_addr)\n    .await\n    .context(\"failed to bind tcp listener\")?;\n\n  axum::serve(listener, app).await.context(\"server crashed\")\n}\n\n#[tokio::main]\nasync fn main() -> anyhow::Result<()> {\n  let mut term_signal = tokio::signal::unix::signal(\n    tokio::signal::unix::SignalKind::terminate(),\n  )?;\n\n  let app = tokio::spawn(app());\n\n  tokio::select! {\n    res = app => return res?,\n    _ = term_signal.recv() => {},\n  }\n\n  Ok(())\n}\n"
  },
  {
    "path": "example/update_logger/Cargo.toml",
    "content": "[package]\nname = \"update_logger\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n# local\nkomodo_client.workspace = true\nlogger.workspace = true\n# external\ntokio.workspace = true\ntracing.workspace = true\nanyhow.workspace = true"
  },
  {
    "path": "example/update_logger/Dockerfile",
    "content": "FROM rust:1.89.0 as builder\nWORKDIR /builder\n\nCOPY . .\n\nRUN cargo build -p update_logger --release\n\nFROM gcr.io/distroless/debian-cc\n\nCOPY --from=builder /builder/target/release/update_logger /\n\nEXPOSE 7000\n\nCMD [\"./update_logger\"]"
  },
  {
    "path": "example/update_logger/src/main.rs",
    "content": "#[macro_use]\nextern crate tracing;\n\nuse komodo_client::{ws::UpdateWsMessage, KomodoClient};\n\n/// Entrypoint for handling each incoming update.\nasync fn handle_incoming_update(update: UpdateWsMessage) {\n  info!(\"{update:?}\");\n}\n\n/// ========================\n/// Ws Listener boilerplate.\n/// ========================\n\nasync fn app() -> anyhow::Result<()> {\n  logger::init(&Default::default())?;\n\n  info!(\"v {}\", env!(\"CARGO_PKG_VERSION\"));\n\n  let komodo =\n    KomodoClient::new_from_env()?.with_healthcheck().await?;\n\n  let (mut rx, _) = komodo.subscribe_to_updates()?;\n\n  loop {\n    let update = match rx.recv().await {\n      Ok(msg) => msg,\n      Err(e) => {\n        error!(\"🚨 recv error | {e:?}\");\n        break;\n      }\n    };\n    handle_incoming_update(update).await\n  }\n\n  Ok(())\n}\n\n#[tokio::main]\nasync fn main() -> anyhow::Result<()> {\n  let mut term_signal = tokio::signal::unix::signal(\n    tokio::signal::unix::SignalKind::terminate(),\n  )?;\n\n  let app = tokio::spawn(app());\n\n  tokio::select! {\n    res = app => return res?,\n    _ = term_signal.recv() => {},\n  }\n\n  Ok(())\n}\n"
  },
  {
    "path": "expose.compose.yaml",
    "content": "services:\n  core:\n    ports:\n      - 9120:9120\n    environment:\n      KOMODO_FIRST_SERVER: http://periphery:8120"
  },
  {
    "path": "frontend/.eslintrc.cjs",
    "content": "module.exports = {\n  root: true,\n  env: { browser: true, es2020: true },\n  extends: [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"plugin:react-hooks/recommended\",\n  ],\n  ignorePatterns: [\"dist\", \".eslintrc.cjs\"],\n  parser: \"@typescript-eslint/parser\",\n  plugins: [\"react-refresh\"],\n  rules: {\n    \"react-refresh/only-export-components\": [\n      \"warn\",\n      { allowConstantExport: true },\n    ],\n    \"@typescript-eslint/no-explicit-any\": \"off\",\n  },\n};\n"
  },
  {
    "path": "frontend/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "frontend/Dockerfile",
    "content": "FROM node:20.12-alpine AS builder\n\nWORKDIR /builder\n\nCOPY ./frontend ./frontend\nCOPY ./client/core/ts ./client\n\n# Optionally specify a specific Komodo host.\nARG VITE_KOMODO_HOST=\"\"\nENV VITE_KOMODO_HOST=$VITE_KOMODO_HOST\n\n# Build and link the client\nRUN cd client && yarn && yarn build && yarn link\nRUN cd frontend && yarn link komodo_client && yarn && yarn build\n\n# Copy just the static frontend to scratch image\nFROM scratch\n\nCOPY --from=builder /builder/frontend/dist /frontend\n\nLABEL org.opencontainers.image.source=https://github.com/moghtech/komodo\nLABEL org.opencontainers.image.description=\"Komodo Frontend\"\nLABEL org.opencontainers.image.licenses=GPL-3.0"
  },
  {
    "path": "frontend/README.md",
    "content": "# Komodo Frontend\n\nKomodo JS stack uses Yarn + Vite + React + Tailwind + shadcn/ui\n\n## Setup Dev Environment\n\nThe frontend depends on the local package `komodo_client` located at `/client/core/ts`.\nThis must first be built and prepared for yarn link.\n\nThe following command should setup everything up (run with /frontend as working directory):\n\n```sh\ncd ../client/core/ts && yarn && yarn build && yarn link && \\\ncd ../../../frontend && yarn link komodo_client && yarn\n```\n\nYou can make a new file `.env.development` (gitignored) which holds:\n```sh\nVITE_KOMODO_HOST=https://demo.komo.do\n```\nYou can point it to any Komodo host you like, including the demo.\n\nNow you can start the dev frontend server:\n```sh\nyarn dev\n```"
  },
  {
    "path": "frontend/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"src/globals.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/\",\n    \"utils\": \"@lib/utils\"\n  }\n}"
  },
  {
    "path": "frontend/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    \n    <link rel=\"icon\" type=\"image/png\" sizes=\"96x96\" href=\"/favicon-96x96.png\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\" />\n    <link rel=\"shortcut icon\" type=\"image/ico\" href=\"/favicon.ico\" />\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n    <meta name=\"apple-mobile-web-app-title\" content=\"Komodo\" />\n    <link rel=\"manifest\" href=\"/manifest.json\" />\n\n    <title>Komodo</title>\n  </head>\n  <body class=\"bg-background\">\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n  \"name\": \"frontend\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite --host\",\n    \"build\": \"tsc && vite build\",\n    \"lint\": \"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0\",\n    \"preview\": \"vite preview\",\n    \"build-client\": \"cd ../client/core/ts && yarn && yarn build && yarn link\"\n  },\n  \"dependencies\": {\n    \"@floating-ui/react\": \"0.27.9\",\n    \"@monaco-editor/react\": \"4.7.0\",\n    \"@radix-ui/react-checkbox\": \"1.3.2\",\n    \"@radix-ui/react-dialog\": \"1.1.14\",\n    \"@radix-ui/react-dropdown-menu\": \"2.1.15\",\n    \"@radix-ui/react-hover-card\": \"1.1.14\",\n    \"@radix-ui/react-icons\": \"1.3.2\",\n    \"@radix-ui/react-label\": \"2.1.7\",\n    \"@radix-ui/react-popover\": \"1.1.14\",\n    \"@radix-ui/react-progress\": \"1.1.7\",\n    \"@radix-ui/react-select\": \"2.2.5\",\n    \"@radix-ui/react-separator\": \"1.1.7\",\n    \"@radix-ui/react-slot\": \"1.2.3\",\n    \"@radix-ui/react-switch\": \"1.2.5\",\n    \"@radix-ui/react-tabs\": \"1.1.12\",\n    \"@radix-ui/react-toast\": \"1.2.14\",\n    \"@radix-ui/react-toggle\": \"1.1.9\",\n    \"@radix-ui/react-toggle-group\": \"1.1.10\",\n    \"@tanstack/react-query\": \"5.77.2\",\n    \"@tanstack/react-table\": \"8.21.3\",\n    \"@xterm/addon-fit\": \"0.10.0\",\n    \"@xterm/xterm\": \"5.5.0\",\n    \"ansi-to-html\": \"0.7.2\",\n    \"class-variance-authority\": \"0.7.1\",\n    \"clsx\": \"2.1.1\",\n    \"cmdk\": \"1.1.1\",\n    \"jotai\": \"2.12.5\",\n    \"lucide-react\": \"0.511.0\",\n    \"monaco-editor\": \"0.52.2\",\n    \"monaco-yaml\": \"5.4.0\",\n    \"prettier\": \"3.5.3\",\n    \"react\": \"19.1.0\",\n    \"react-charts\": \"3.0.0-beta.57\",\n    \"react-dom\": \"19.1.0\",\n    \"react-minimal-pie-chart\": \"9.1.0\",\n    \"react-router-dom\": \"7.6.1\",\n    \"react-xtermjs\": \"1.0.10\",\n    \"sanitize-html\": \"2.17.0\",\n    \"tailwind-merge\": \"2.6.0\",\n    \"tailwindcss-animate\": \"1.0.7\",\n    \"shell-quote\": \"1.8.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"19.1.6\",\n    \"@types/react-dom\": \"19.1.5\",\n    \"@types/sanitize-html\": \"2.16.0\",\n    \"@typescript-eslint/eslint-plugin\": \"8.33.0\",\n    \"@typescript-eslint/parser\": \"8.33.0\",\n    \"@vitejs/plugin-react\": \"4.5.0\",\n    \"autoprefixer\": \"10.4.21\",\n    \"eslint\": \"9.27.0\",\n    \"eslint-plugin-react-hooks\": \"5.2.0\",\n    \"eslint-plugin-react-refresh\": \"0.4.20\",\n    \"postcss\": \"8.5.3\",\n    \"tailwindcss\": \"3.4.17\",\n    \"typescript\": \"5.8.3\",\n    \"vite\": \"6.0.7\",\n    \"vite-tsconfig-paths\": \"5.1.4\"\n  },\n  \"packageManager\": \"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e\"\n}\n"
  },
  {
    "path": "frontend/postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "frontend/public/client/lib.d.ts",
    "content": "import { AuthResponses, ExecuteResponses, ReadResponses, UserResponses, WriteResponses } from \"./responses.js\";\nimport { ConnectExecQuery, ExecuteExecBody, TerminalCallbacks } from \"./terminal.js\";\nimport { AuthRequest, ConnectTerminalQuery, ExecuteRequest, ExecuteTerminalBody, ReadRequest, Update, UpdateListItem, UserRequest, WriteRequest } from \"./types.js\";\nexport * as Types from \"./types.js\";\nexport type { ConnectExecQuery, ExecuteExecBody, TerminalCallbacks };\nexport type InitOptions = {\n    type: \"jwt\";\n    params: {\n        jwt: string;\n    };\n} | {\n    type: \"api-key\";\n    params: {\n        key: string;\n        secret: string;\n    };\n};\nexport declare class CancelToken {\n    cancelled: boolean;\n    constructor();\n    cancel(): void;\n}\nexport type ClientState = {\n    jwt: string | undefined;\n    key: string | undefined;\n    secret: string | undefined;\n};\n/** Initialize a new client for Komodo */\nexport declare function KomodoClient(url: string, options: InitOptions): {\n    /**\n     * Call the `/auth` api.\n     *\n     * ```\n     * const login_options = await komodo.auth(\"GetLoginOptions\", {});\n     * ```\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/auth/index.html\n     */\n    auth: <T extends AuthRequest[\"type\"], Req extends Extract<AuthRequest, {\n        type: T;\n    }>>(type: T, params: Req[\"params\"]) => Promise<AuthResponses[Req[\"type\"]]>;\n    /**\n     * Call the `/user` api.\n     *\n     * ```\n     * const { key, secret } = await komodo.user(\"CreateApiKey\", {\n     *   name: \"my-api-key\"\n     * });\n     * ```\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/user/index.html\n     */\n    user: <T extends UserRequest[\"type\"], Req extends Extract<UserRequest, {\n        type: T;\n    }>>(type: T, params: Req[\"params\"]) => Promise<UserResponses[Req[\"type\"]]>;\n    /**\n     * Call the `/read` api.\n     *\n     * ```\n     * const stack = await komodo.read(\"GetStack\", {\n     *   stack: \"my-stack\"\n     * });\n     * ```\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/read/index.html\n     */\n    read: <T extends ReadRequest[\"type\"], Req extends Extract<ReadRequest, {\n        type: T;\n    }>>(type: T, params: Req[\"params\"]) => Promise<ReadResponses[Req[\"type\"]]>;\n    /**\n     * Call the `/write` api.\n     *\n     * ```\n     * const build = await komodo.write(\"UpdateBuild\", {\n     *   id: \"my-build\",\n     *   config: {\n     *     version: \"1.0.4\"\n     *   }\n     * });\n     * ```\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/write/index.html\n     */\n    write: <T extends WriteRequest[\"type\"], Req extends Extract<WriteRequest, {\n        type: T;\n    }>>(type: T, params: Req[\"params\"]) => Promise<WriteResponses[Req[\"type\"]]>;\n    /**\n     * Call the `/execute` api.\n     *\n     * ```\n     * const update = await komodo.execute(\"DeployStack\", {\n     *   stack: \"my-stack\"\n     * });\n     * ```\n     *\n     * NOTE. These calls return immediately when the update is created, NOT when the execution task finishes.\n     * To have the call only return when the task finishes, use [execute_and_poll_until_complete].\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html\n     */\n    execute: <T extends ExecuteRequest[\"type\"], Req extends Extract<ExecuteRequest, {\n        type: T;\n    }>>(type: T, params: Req[\"params\"]) => Promise<ExecuteResponses[Req[\"type\"]]>;\n    /**\n     * Call the `/execute` api, and poll the update until the task has completed.\n     *\n     * ```\n     * const update = await komodo.execute_and_poll(\"DeployStack\", {\n     *   stack: \"my-stack\"\n     * });\n     * ```\n     *\n     * https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html\n     */\n    execute_and_poll: <T extends ExecuteRequest[\"type\"], Req extends Extract<ExecuteRequest, {\n        type: T;\n    }>>(type: T, params: Req[\"params\"]) => Promise<Update | (Update | {\n        status: \"Err\";\n        data: import(\"./types.js\").BatchExecutionResponseItemErr;\n    })[]>;\n    /**\n     * Poll an Update (returned by the `execute` calls) until the `status` is `Complete`.\n     * https://docs.rs/komodo_client/latest/komodo_client/entities/update/struct.Update.html#structfield.status.\n     */\n    poll_update_until_complete: (update_id: string) => Promise<Update>;\n    /** Returns the version of Komodo Core the client is calling to. */\n    core_version: () => Promise<string>;\n    /**\n     * Connects to update websocket, performs login and attaches handlers,\n     * and returns the WebSocket handle.\n     */\n    get_update_websocket: ({ on_update, on_login, on_open, on_close, }: {\n        on_update: (update: UpdateListItem) => void;\n        on_login?: () => void;\n        on_open?: () => void;\n        on_close?: () => void;\n    }) => WebSocket;\n    /**\n     * Subscribes to the update websocket with automatic reconnect loop.\n     *\n     * Note. Awaiting this method will never finish.\n     */\n    subscribe_to_update_websocket: ({ on_update, on_open, on_login, on_close, retry, retry_timeout_ms, cancel, on_cancel, }: {\n        on_update: (update: UpdateListItem) => void;\n        on_login?: () => void;\n        on_open?: () => void;\n        on_close?: () => void;\n        retry?: boolean;\n        retry_timeout_ms?: number;\n        cancel?: CancelToken;\n        on_cancel?: () => void;\n    }) => Promise<void>;\n    /**\n     * Subscribes to terminal io over websocket message,\n     * for use with xtermjs.\n     */\n    connect_terminal: ({ query, on_message, on_login, on_open, on_close, }: {\n        query: ConnectTerminalQuery;\n    } & TerminalCallbacks) => WebSocket;\n    /**\n     * Executes a command on a given Server / terminal,\n     * and gives a callback to handle the output as it comes in.\n     *\n     * ```ts\n     * await komodo.execute_terminal(\n     *   {\n     *     server: \"my-server\",\n     *     terminal: \"name\",\n     *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     *   },\n     *   {\n     *     onLine: (line) => console.log(line),\n     *     onFinish: (code) => console.log(\"Finished:\", code),\n     *   }\n     * );\n     * ```\n     */\n    execute_terminal: (request: ExecuteTerminalBody, callbacks?: import(\"./terminal.js\").ExecuteCallbacks) => Promise<void>;\n    /**\n     * Executes a command on a given Server / terminal,\n     * and returns a stream to process the output as it comes in.\n     *\n     * Note. The final line of the stream will usually be\n     * `__KOMODO_EXIT_CODE__:0`. The number\n     * is the exit code of the command.\n     *\n     * If this line is NOT present, it means the stream\n     * was terminated early, ie like running `exit`.\n     *\n     * ```ts\n     * const stream = await komodo.execute_terminal_stream({\n     *   server: \"my-server\",\n     *   terminal: \"name\",\n     *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     * });\n     *\n     * for await (const line of stream) {\n     *   console.log(line);\n     * }\n     * ```\n     */\n    execute_terminal_stream: (request: ExecuteTerminalBody) => Promise<AsyncIterable<string>>;\n    /**\n     * Subscribes to container exec io over websocket message,\n     * for use with xtermjs. Can connect to container on a Server,\n     * or associated with a Deployment or Stack.\n     * Terminal permission on connecting resource required.\n     */\n    connect_exec: ({ query: { type, query }, on_message, on_login, on_open, on_close, }: {\n        query: ConnectExecQuery;\n    } & TerminalCallbacks) => WebSocket;\n    /**\n     * Subscribes to container exec io over websocket message,\n     * for use with xtermjs. Can connect to Container on a Server.\n     * Server Terminal permission required.\n     */\n    connect_container_exec: ({ query, ...callbacks }: {\n        query: import(\"./types.js\").ConnectContainerExecQuery;\n    } & TerminalCallbacks) => WebSocket;\n    /**\n     * Executes a command on a given container,\n     * and gives a callback to handle the output as it comes in.\n     *\n     * ```ts\n     * await komodo.execute_container_exec(\n     *   {\n     *     server: \"my-server\",\n     *     container: \"name\",\n     *     shell: \"bash\",\n     *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     *   },\n     *   {\n     *     onLine: (line) => console.log(line),\n     *     onFinish: (code) => console.log(\"Finished:\", code),\n     *   }\n     * );\n     * ```\n     */\n    execute_container_exec: (body: import(\"./types.js\").ExecuteContainerExecBody, callbacks?: import(\"./terminal.js\").ExecuteCallbacks) => Promise<void>;\n    /**\n     * Executes a command on a given container,\n     * and returns a stream to process the output as it comes in.\n     *\n     * Note. The final line of the stream will usually be\n     * `__KOMODO_EXIT_CODE__:0`. The number\n     * is the exit code of the command.\n     *\n     * If this line is NOT present, it means the stream\n     * was terminated early, ie like running `exit`.\n     *\n     * ```ts\n     * const stream = await komodo.execute_container_exec_stream({\n     *   server: \"my-server\",\n     *   container: \"name\",\n     *   shell: \"bash\",\n     *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     * });\n     *\n     * for await (const line of stream) {\n     *   console.log(line);\n     * }\n     * ```\n     */\n    execute_container_exec_stream: (body: import(\"./types.js\").ExecuteContainerExecBody) => Promise<AsyncIterable<string>>;\n    /**\n     * Subscribes to deployment container exec io over websocket message,\n     * for use with xtermjs. Can connect to Deployment container.\n     * Deployment Terminal permission required.\n     */\n    connect_deployment_exec: ({ query, ...callbacks }: {\n        query: import(\"./types.js\").ConnectDeploymentExecQuery;\n    } & TerminalCallbacks) => WebSocket;\n    /**\n     * Executes a command on a given deployment container,\n     * and gives a callback to handle the output as it comes in.\n     *\n     * ```ts\n     * await komodo.execute_deployment_exec(\n     *   {\n     *     deployment: \"my-deployment\",\n     *     shell: \"bash\",\n     *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     *   },\n     *   {\n     *     onLine: (line) => console.log(line),\n     *     onFinish: (code) => console.log(\"Finished:\", code),\n     *   }\n     * );\n     * ```\n     */\n    execute_deployment_exec: (body: import(\"./types.js\").ExecuteDeploymentExecBody, callbacks?: import(\"./terminal.js\").ExecuteCallbacks) => Promise<void>;\n    /**\n     * Executes a command on a given deployment container,\n     * and returns a stream to process the output as it comes in.\n     *\n     * Note. The final line of the stream will usually be\n     * `__KOMODO_EXIT_CODE__:0`. The number\n     * is the exit code of the command.\n     *\n     * If this line is NOT present, it means the stream\n     * was terminated early, ie like running `exit`.\n     *\n     * ```ts\n     * const stream = await komodo.execute_deployment_exec_stream({\n     *   deployment: \"my-deployment\",\n     *   shell: \"bash\",\n     *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     * });\n     *\n     * for await (const line of stream) {\n     *   console.log(line);\n     * }\n     * ```\n     */\n    execute_deployment_exec_stream: (body: import(\"./types.js\").ExecuteDeploymentExecBody) => Promise<AsyncIterable<string>>;\n    /**\n     * Subscribes to container exec io over websocket message,\n     * for use with xtermjs. Can connect to Stack service container.\n     * Stack Terminal permission required.\n     */\n    connect_stack_exec: ({ query, ...callbacks }: {\n        query: import(\"./types.js\").ConnectStackExecQuery;\n    } & TerminalCallbacks) => WebSocket;\n    /**\n     * Executes a command on a given stack service container,\n     * and gives a callback to handle the output as it comes in.\n     *\n     * ```ts\n     * await komodo.execute_stack_exec(\n     *   {\n     *     stack: \"my-stack\",\n     *     service: \"database\"\n     *     shell: \"bash\",\n     *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     *   },\n     *   {\n     *     onLine: (line) => console.log(line),\n     *     onFinish: (code) => console.log(\"Finished:\", code),\n     *   }\n     * );\n     * ```\n     */\n    execute_stack_exec: (body: import(\"./types.js\").ExecuteStackExecBody, callbacks?: import(\"./terminal.js\").ExecuteCallbacks) => Promise<void>;\n    /**\n     * Executes a command on a given stack service container,\n     * and returns a stream to process the output as it comes in.\n     *\n     * Note. The final line of the stream will usually be\n     * `__KOMODO_EXIT_CODE__:0`. The number\n     * is the exit code of the command.\n     *\n     * If this line is NOT present, it means the stream\n     * was terminated early, ie like running `exit`.\n     *\n     * ```ts\n     * const stream = await komodo.execute_stack_exec_stream({\n     *   stack: \"my-stack\",\n     *   service: \"service1\",\n     *   shell: \"bash\",\n     *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n     * });\n     *\n     * for await (const line of stream) {\n     *   console.log(line);\n     * }\n     * ```\n     */\n    execute_stack_exec_stream: (body: import(\"./types.js\").ExecuteStackExecBody) => Promise<AsyncIterable<string>>;\n};\n"
  },
  {
    "path": "frontend/public/client/lib.js",
    "content": "import { terminal_methods, } from \"./terminal.js\";\nimport { UpdateStatus, } from \"./types.js\";\nexport * as Types from \"./types.js\";\nexport class CancelToken {\n    cancelled;\n    constructor() {\n        this.cancelled = false;\n    }\n    cancel() {\n        this.cancelled = true;\n    }\n}\n/** Initialize a new client for Komodo */\nexport function KomodoClient(url, options) {\n    const state = {\n        jwt: options.type === \"jwt\" ? options.params.jwt : undefined,\n        key: options.type === \"api-key\" ? options.params.key : undefined,\n        secret: options.type === \"api-key\" ? options.params.secret : undefined,\n    };\n    const request = (path, type, params) => new Promise(async (res, rej) => {\n        try {\n            let response = await fetch(`${url}${path}/${type}`, {\n                method: \"POST\",\n                body: JSON.stringify(params),\n                headers: {\n                    ...(state.jwt\n                        ? {\n                            authorization: state.jwt,\n                        }\n                        : state.key && state.secret\n                            ? {\n                                \"x-api-key\": state.key,\n                                \"x-api-secret\": state.secret,\n                            }\n                            : {}),\n                    \"content-type\": \"application/json\",\n                },\n            });\n            if (response.status === 200) {\n                const body = await response.json();\n                res(body);\n            }\n            else {\n                try {\n                    const result = await response.json();\n                    rej({ status: response.status, result });\n                }\n                catch (error) {\n                    rej({\n                        status: response.status,\n                        result: {\n                            error: \"Failed to get response body\",\n                            trace: [JSON.stringify(error)],\n                        },\n                        error,\n                    });\n                }\n            }\n        }\n        catch (error) {\n            rej({\n                status: 1,\n                result: {\n                    error: \"Request failed with error\",\n                    trace: [JSON.stringify(error)],\n                },\n                error,\n            });\n        }\n    });\n    const auth = async (type, params) => await request(\"/auth\", type, params);\n    const user = async (type, params) => await request(\"/user\", type, params);\n    const read = async (type, params) => await request(\"/read\", type, params);\n    const write = async (type, params) => await request(\"/write\", type, params);\n    const execute = async (type, params) => await request(\"/execute\", type, params);\n    const execute_and_poll = async (type, params) => {\n        const res = await execute(type, params);\n        // Check if its a batch of updates or a single update;\n        if (Array.isArray(res)) {\n            const batch = res;\n            return await Promise.all(batch.map(async (item) => {\n                if (item.status === \"Err\") {\n                    return item;\n                }\n                return await poll_update_until_complete(item.data._id?.$oid);\n            }));\n        }\n        else {\n            // it is a single update\n            const update = res;\n            if (update.status === UpdateStatus.Complete || !update._id?.$oid) {\n                return update;\n            }\n            return await poll_update_until_complete(update._id?.$oid);\n        }\n    };\n    const poll_update_until_complete = async (update_id) => {\n        while (true) {\n            await new Promise((resolve) => setTimeout(resolve, 1000));\n            const update = await read(\"GetUpdate\", { id: update_id });\n            if (update.status === UpdateStatus.Complete) {\n                return update;\n            }\n        }\n    };\n    const core_version = () => read(\"GetVersion\", {}).then((res) => res.version);\n    const get_update_websocket = ({ on_update, on_login, on_open, on_close, }) => {\n        const ws = new WebSocket(url.replace(\"http\", \"ws\") + \"/ws/update\");\n        // Handle login on websocket open\n        ws.addEventListener(\"open\", () => {\n            on_open?.();\n            const login_msg = options.type === \"jwt\"\n                ? {\n                    type: \"Jwt\",\n                    params: {\n                        jwt: options.params.jwt,\n                    },\n                }\n                : {\n                    type: \"ApiKeys\",\n                    params: {\n                        key: options.params.key,\n                        secret: options.params.secret,\n                    },\n                };\n            ws.send(JSON.stringify(login_msg));\n        });\n        ws.addEventListener(\"message\", ({ data }) => {\n            if (data == \"LOGGED_IN\")\n                return on_login?.();\n            on_update(JSON.parse(data));\n        });\n        if (on_close) {\n            ws.addEventListener(\"close\", on_close);\n        }\n        return ws;\n    };\n    const subscribe_to_update_websocket = async ({ on_update, on_open, on_login, on_close, retry = true, retry_timeout_ms = 5_000, cancel = new CancelToken(), on_cancel, }) => {\n        while (true) {\n            if (cancel.cancelled) {\n                on_cancel?.();\n                return;\n            }\n            try {\n                const ws = get_update_websocket({\n                    on_open,\n                    on_login,\n                    on_update,\n                    on_close,\n                });\n                // This while loop will end when the socket is closed\n                while (ws.readyState !== WebSocket.CLOSING &&\n                    ws.readyState !== WebSocket.CLOSED) {\n                    if (cancel.cancelled)\n                        ws.close();\n                    // Sleep for a bit before checking for websocket closed\n                    await new Promise((resolve) => setTimeout(resolve, 500));\n                }\n                if (retry) {\n                    // Sleep for a bit before retrying connection to avoid spam.\n                    await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));\n                }\n                else {\n                    return;\n                }\n            }\n            catch (error) {\n                console.error(error);\n                if (retry) {\n                    // Sleep for a bit before retrying, maybe Komodo Core is down temporarily.\n                    await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));\n                }\n                else {\n                    return;\n                }\n            }\n        }\n    };\n    const { connect_terminal, execute_terminal, execute_terminal_stream, connect_exec, connect_container_exec, execute_container_exec, execute_container_exec_stream, connect_deployment_exec, execute_deployment_exec, execute_deployment_exec_stream, connect_stack_exec, execute_stack_exec, execute_stack_exec_stream, } = terminal_methods(url, state);\n    return {\n        /**\n         * Call the `/auth` api.\n         *\n         * ```\n         * const login_options = await komodo.auth(\"GetLoginOptions\", {});\n         * ```\n         *\n         * https://docs.rs/komodo_client/latest/komodo_client/api/auth/index.html\n         */\n        auth,\n        /**\n         * Call the `/user` api.\n         *\n         * ```\n         * const { key, secret } = await komodo.user(\"CreateApiKey\", {\n         *   name: \"my-api-key\"\n         * });\n         * ```\n         *\n         * https://docs.rs/komodo_client/latest/komodo_client/api/user/index.html\n         */\n        user,\n        /**\n         * Call the `/read` api.\n         *\n         * ```\n         * const stack = await komodo.read(\"GetStack\", {\n         *   stack: \"my-stack\"\n         * });\n         * ```\n         *\n         * https://docs.rs/komodo_client/latest/komodo_client/api/read/index.html\n         */\n        read,\n        /**\n         * Call the `/write` api.\n         *\n         * ```\n         * const build = await komodo.write(\"UpdateBuild\", {\n         *   id: \"my-build\",\n         *   config: {\n         *     version: \"1.0.4\"\n         *   }\n         * });\n         * ```\n         *\n         * https://docs.rs/komodo_client/latest/komodo_client/api/write/index.html\n         */\n        write,\n        /**\n         * Call the `/execute` api.\n         *\n         * ```\n         * const update = await komodo.execute(\"DeployStack\", {\n         *   stack: \"my-stack\"\n         * });\n         * ```\n         *\n         * NOTE. These calls return immediately when the update is created, NOT when the execution task finishes.\n         * To have the call only return when the task finishes, use [execute_and_poll_until_complete].\n         *\n         * https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html\n         */\n        execute,\n        /**\n         * Call the `/execute` api, and poll the update until the task has completed.\n         *\n         * ```\n         * const update = await komodo.execute_and_poll(\"DeployStack\", {\n         *   stack: \"my-stack\"\n         * });\n         * ```\n         *\n         * https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html\n         */\n        execute_and_poll,\n        /**\n         * Poll an Update (returned by the `execute` calls) until the `status` is `Complete`.\n         * https://docs.rs/komodo_client/latest/komodo_client/entities/update/struct.Update.html#structfield.status.\n         */\n        poll_update_until_complete,\n        /** Returns the version of Komodo Core the client is calling to. */\n        core_version,\n        /**\n         * Connects to update websocket, performs login and attaches handlers,\n         * and returns the WebSocket handle.\n         */\n        get_update_websocket,\n        /**\n         * Subscribes to the update websocket with automatic reconnect loop.\n         *\n         * Note. Awaiting this method will never finish.\n         */\n        subscribe_to_update_websocket,\n        /**\n         * Subscribes to terminal io over websocket message,\n         * for use with xtermjs.\n         */\n        connect_terminal,\n        /**\n         * Executes a command on a given Server / terminal,\n         * and gives a callback to handle the output as it comes in.\n         *\n         * ```ts\n         * await komodo.execute_terminal(\n         *   {\n         *     server: \"my-server\",\n         *     terminal: \"name\",\n         *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n         *   },\n         *   {\n         *     onLine: (line) => console.log(line),\n         *     onFinish: (code) => console.log(\"Finished:\", code),\n         *   }\n         * );\n         * ```\n         */\n        execute_terminal,\n        /**\n         * Executes a command on a given Server / terminal,\n         * and returns a stream to process the output as it comes in.\n         *\n         * Note. The final line of the stream will usually be\n         * `__KOMODO_EXIT_CODE__:0`. The number\n         * is the exit code of the command.\n         *\n         * If this line is NOT present, it means the stream\n         * was terminated early, ie like running `exit`.\n         *\n         * ```ts\n         * const stream = await komodo.execute_terminal_stream({\n         *   server: \"my-server\",\n         *   terminal: \"name\",\n         *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n         * });\n         *\n         * for await (const line of stream) {\n         *   console.log(line);\n         * }\n         * ```\n         */\n        execute_terminal_stream,\n        /**\n         * Subscribes to container exec io over websocket message,\n         * for use with xtermjs. Can connect to container on a Server,\n         * or associated with a Deployment or Stack.\n         * Terminal permission on connecting resource required.\n         */\n        connect_exec,\n        /**\n         * Subscribes to container exec io over websocket message,\n         * for use with xtermjs. Can connect to Container on a Server.\n         * Server Terminal permission required.\n         */\n        connect_container_exec,\n        /**\n         * Executes a command on a given container,\n         * and gives a callback to handle the output as it comes in.\n         *\n         * ```ts\n         * await komodo.execute_container_exec(\n         *   {\n         *     server: \"my-server\",\n         *     container: \"name\",\n         *     shell: \"bash\",\n         *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n         *   },\n         *   {\n         *     onLine: (line) => console.log(line),\n         *     onFinish: (code) => console.log(\"Finished:\", code),\n         *   }\n         * );\n         * ```\n         */\n        execute_container_exec,\n        /**\n         * Executes a command on a given container,\n         * and returns a stream to process the output as it comes in.\n         *\n         * Note. The final line of the stream will usually be\n         * `__KOMODO_EXIT_CODE__:0`. The number\n         * is the exit code of the command.\n         *\n         * If this line is NOT present, it means the stream\n         * was terminated early, ie like running `exit`.\n         *\n         * ```ts\n         * const stream = await komodo.execute_container_exec_stream({\n         *   server: \"my-server\",\n         *   container: \"name\",\n         *   shell: \"bash\",\n         *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n         * });\n         *\n         * for await (const line of stream) {\n         *   console.log(line);\n         * }\n         * ```\n         */\n        execute_container_exec_stream,\n        /**\n         * Subscribes to deployment container exec io over websocket message,\n         * for use with xtermjs. Can connect to Deployment container.\n         * Deployment Terminal permission required.\n         */\n        connect_deployment_exec,\n        /**\n         * Executes a command on a given deployment container,\n         * and gives a callback to handle the output as it comes in.\n         *\n         * ```ts\n         * await komodo.execute_deployment_exec(\n         *   {\n         *     deployment: \"my-deployment\",\n         *     shell: \"bash\",\n         *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n         *   },\n         *   {\n         *     onLine: (line) => console.log(line),\n         *     onFinish: (code) => console.log(\"Finished:\", code),\n         *   }\n         * );\n         * ```\n         */\n        execute_deployment_exec,\n        /**\n         * Executes a command on a given deployment container,\n         * and returns a stream to process the output as it comes in.\n         *\n         * Note. The final line of the stream will usually be\n         * `__KOMODO_EXIT_CODE__:0`. The number\n         * is the exit code of the command.\n         *\n         * If this line is NOT present, it means the stream\n         * was terminated early, ie like running `exit`.\n         *\n         * ```ts\n         * const stream = await komodo.execute_deployment_exec_stream({\n         *   deployment: \"my-deployment\",\n         *   shell: \"bash\",\n         *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n         * });\n         *\n         * for await (const line of stream) {\n         *   console.log(line);\n         * }\n         * ```\n         */\n        execute_deployment_exec_stream,\n        /**\n         * Subscribes to container exec io over websocket message,\n         * for use with xtermjs. Can connect to Stack service container.\n         * Stack Terminal permission required.\n         */\n        connect_stack_exec,\n        /**\n         * Executes a command on a given stack service container,\n         * and gives a callback to handle the output as it comes in.\n         *\n         * ```ts\n         * await komodo.execute_stack_exec(\n         *   {\n         *     stack: \"my-stack\",\n         *     service: \"database\"\n         *     shell: \"bash\",\n         *     command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n         *   },\n         *   {\n         *     onLine: (line) => console.log(line),\n         *     onFinish: (code) => console.log(\"Finished:\", code),\n         *   }\n         * );\n         * ```\n         */\n        execute_stack_exec,\n        /**\n         * Executes a command on a given stack service container,\n         * and returns a stream to process the output as it comes in.\n         *\n         * Note. The final line of the stream will usually be\n         * `__KOMODO_EXIT_CODE__:0`. The number\n         * is the exit code of the command.\n         *\n         * If this line is NOT present, it means the stream\n         * was terminated early, ie like running `exit`.\n         *\n         * ```ts\n         * const stream = await komodo.execute_stack_exec_stream({\n         *   stack: \"my-stack\",\n         *   service: \"service1\",\n         *   shell: \"bash\",\n         *   command: 'for i in {1..3}; do echo \"$i\"; sleep 1; done',\n         * });\n         *\n         * for await (const line of stream) {\n         *   console.log(line);\n         * }\n         * ```\n         */\n        execute_stack_exec_stream,\n    };\n}\n"
  },
  {
    "path": "frontend/public/client/responses.d.ts",
    "content": "import * as Types from \"./types.js\";\nexport type AuthResponses = {\n    GetLoginOptions: Types.GetLoginOptionsResponse;\n    SignUpLocalUser: Types.SignUpLocalUserResponse;\n    LoginLocalUser: Types.LoginLocalUserResponse;\n    ExchangeForJwt: Types.ExchangeForJwtResponse;\n    GetUser: Types.GetUserResponse;\n};\nexport type UserResponses = {\n    PushRecentlyViewed: Types.PushRecentlyViewedResponse;\n    SetLastSeenUpdate: Types.SetLastSeenUpdateResponse;\n    CreateApiKey: Types.CreateApiKeyResponse;\n    DeleteApiKey: Types.DeleteApiKeyResponse;\n};\nexport type ReadResponses = {\n    GetVersion: Types.GetVersionResponse;\n    GetCoreInfo: Types.GetCoreInfoResponse;\n    ListSecrets: Types.ListSecretsResponse;\n    ListGitProvidersFromConfig: Types.ListGitProvidersFromConfigResponse;\n    ListDockerRegistriesFromConfig: Types.ListDockerRegistriesFromConfigResponse;\n    GetUsername: Types.GetUsernameResponse;\n    GetPermission: Types.GetPermissionResponse;\n    FindUser: Types.FindUserResponse;\n    ListUsers: Types.ListUsersResponse;\n    ListApiKeys: Types.ListApiKeysResponse;\n    ListApiKeysForServiceUser: Types.ListApiKeysForServiceUserResponse;\n    ListPermissions: Types.ListPermissionsResponse;\n    ListUserTargetPermissions: Types.ListUserTargetPermissionsResponse;\n    GetUserGroup: Types.GetUserGroupResponse;\n    ListUserGroups: Types.ListUserGroupsResponse;\n    GetProceduresSummary: Types.GetProceduresSummaryResponse;\n    GetProcedure: Types.GetProcedureResponse;\n    GetProcedureActionState: Types.GetProcedureActionStateResponse;\n    ListProcedures: Types.ListProceduresResponse;\n    ListFullProcedures: Types.ListFullProceduresResponse;\n    GetActionsSummary: Types.GetActionsSummaryResponse;\n    GetAction: Types.GetActionResponse;\n    GetActionActionState: Types.GetActionActionStateResponse;\n    ListActions: Types.ListActionsResponse;\n    ListFullActions: Types.ListFullActionsResponse;\n    ListSchedules: Types.ListSchedulesResponse;\n    GetServersSummary: Types.GetServersSummaryResponse;\n    GetServer: Types.GetServerResponse;\n    GetServerState: Types.GetServerStateResponse;\n    GetPeripheryVersion: Types.GetPeripheryVersionResponse;\n    GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse;\n    ListDockerContainers: Types.ListDockerContainersResponse;\n    ListAllDockerContainers: Types.ListAllDockerContainersResponse;\n    InspectDockerContainer: Types.InspectDockerContainerResponse;\n    GetResourceMatchingContainer: Types.GetResourceMatchingContainerResponse;\n    GetContainerLog: Types.GetContainerLogResponse;\n    SearchContainerLog: Types.SearchContainerLogResponse;\n    ListDockerNetworks: Types.ListDockerNetworksResponse;\n    InspectDockerNetwork: Types.InspectDockerNetworkResponse;\n    ListDockerImages: Types.ListDockerImagesResponse;\n    InspectDockerImage: Types.InspectDockerImageResponse;\n    ListDockerImageHistory: Types.ListDockerImageHistoryResponse;\n    ListDockerVolumes: Types.ListDockerVolumesResponse;\n    InspectDockerVolume: Types.InspectDockerVolumeResponse;\n    ListComposeProjects: Types.ListComposeProjectsResponse;\n    GetServerActionState: Types.GetServerActionStateResponse;\n    GetHistoricalServerStats: Types.GetHistoricalServerStatsResponse;\n    ListServers: Types.ListServersResponse;\n    ListFullServers: Types.ListFullServersResponse;\n    ListTerminals: Types.ListTerminalsResponse;\n    GetStacksSummary: Types.GetStacksSummaryResponse;\n    GetStack: Types.GetStackResponse;\n    GetStackActionState: Types.GetStackActionStateResponse;\n    GetStackWebhooksEnabled: Types.GetStackWebhooksEnabledResponse;\n    GetStackLog: Types.GetStackLogResponse;\n    SearchStackLog: Types.SearchStackLogResponse;\n    InspectStackContainer: Types.InspectStackContainerResponse;\n    ListStacks: Types.ListStacksResponse;\n    ListFullStacks: Types.ListFullStacksResponse;\n    ListStackServices: Types.ListStackServicesResponse;\n    ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse;\n    ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse;\n    GetDeploymentsSummary: Types.GetDeploymentsSummaryResponse;\n    GetDeployment: Types.GetDeploymentResponse;\n    GetDeploymentContainer: Types.GetDeploymentContainerResponse;\n    GetDeploymentActionState: Types.GetDeploymentActionStateResponse;\n    GetDeploymentStats: Types.GetDeploymentStatsResponse;\n    GetDeploymentLog: Types.GetDeploymentLogResponse;\n    SearchDeploymentLog: Types.SearchDeploymentLogResponse;\n    InspectDeploymentContainer: Types.InspectDeploymentContainerResponse;\n    ListDeployments: Types.ListDeploymentsResponse;\n    ListFullDeployments: Types.ListFullDeploymentsResponse;\n    ListCommonDeploymentExtraArgs: Types.ListCommonDeploymentExtraArgsResponse;\n    GetBuildsSummary: Types.GetBuildsSummaryResponse;\n    GetBuild: Types.GetBuildResponse;\n    GetBuildActionState: Types.GetBuildActionStateResponse;\n    GetBuildMonthlyStats: Types.GetBuildMonthlyStatsResponse;\n    GetBuildWebhookEnabled: Types.GetBuildWebhookEnabledResponse;\n    ListBuilds: Types.ListBuildsResponse;\n    ListFullBuilds: Types.ListFullBuildsResponse;\n    ListBuildVersions: Types.ListBuildVersionsResponse;\n    ListCommonBuildExtraArgs: Types.ListCommonBuildExtraArgsResponse;\n    GetReposSummary: Types.GetReposSummaryResponse;\n    GetRepo: Types.GetRepoResponse;\n    GetRepoActionState: Types.GetRepoActionStateResponse;\n    GetRepoWebhooksEnabled: Types.GetRepoWebhooksEnabledResponse;\n    ListRepos: Types.ListReposResponse;\n    ListFullRepos: Types.ListFullReposResponse;\n    GetResourceSyncsSummary: Types.GetResourceSyncsSummaryResponse;\n    GetResourceSync: Types.GetResourceSyncResponse;\n    GetResourceSyncActionState: Types.GetResourceSyncActionStateResponse;\n    GetSyncWebhooksEnabled: Types.GetSyncWebhooksEnabledResponse;\n    ListResourceSyncs: Types.ListResourceSyncsResponse;\n    ListFullResourceSyncs: Types.ListFullResourceSyncsResponse;\n    GetBuildersSummary: Types.GetBuildersSummaryResponse;\n    GetBuilder: Types.GetBuilderResponse;\n    ListBuilders: Types.ListBuildersResponse;\n    ListFullBuilders: Types.ListFullBuildersResponse;\n    GetAlertersSummary: Types.GetAlertersSummaryResponse;\n    GetAlerter: Types.GetAlerterResponse;\n    ListAlerters: Types.ListAlertersResponse;\n    ListFullAlerters: Types.ListFullAlertersResponse;\n    ExportAllResourcesToToml: Types.ExportAllResourcesToTomlResponse;\n    ExportResourcesToToml: Types.ExportResourcesToTomlResponse;\n    GetTag: Types.GetTagResponse;\n    ListTags: Types.ListTagsResponse;\n    GetUpdate: Types.GetUpdateResponse;\n    ListUpdates: Types.ListUpdatesResponse;\n    ListAlerts: Types.ListAlertsResponse;\n    GetAlert: Types.GetAlertResponse;\n    GetSystemInformation: Types.GetSystemInformationResponse;\n    GetSystemStats: Types.GetSystemStatsResponse;\n    ListSystemProcesses: Types.ListSystemProcessesResponse;\n    GetVariable: Types.GetVariableResponse;\n    ListVariables: Types.ListVariablesResponse;\n    GetGitProviderAccount: Types.GetGitProviderAccountResponse;\n    ListGitProviderAccounts: Types.ListGitProviderAccountsResponse;\n    GetDockerRegistryAccount: Types.GetDockerRegistryAccountResponse;\n    ListDockerRegistryAccounts: Types.ListDockerRegistryAccountsResponse;\n};\nexport type WriteResponses = {\n    CreateLocalUser: Types.CreateLocalUserResponse;\n    UpdateUserUsername: Types.UpdateUserUsernameResponse;\n    UpdateUserPassword: Types.UpdateUserPasswordResponse;\n    DeleteUser: Types.DeleteUserResponse;\n    CreateServiceUser: Types.CreateServiceUserResponse;\n    UpdateServiceUserDescription: Types.UpdateServiceUserDescriptionResponse;\n    CreateApiKeyForServiceUser: Types.CreateApiKeyForServiceUserResponse;\n    DeleteApiKeyForServiceUser: Types.DeleteApiKeyForServiceUserResponse;\n    CreateUserGroup: Types.UserGroup;\n    RenameUserGroup: Types.UserGroup;\n    DeleteUserGroup: Types.UserGroup;\n    AddUserToUserGroup: Types.UserGroup;\n    RemoveUserFromUserGroup: Types.UserGroup;\n    SetUsersInUserGroup: Types.UserGroup;\n    SetEveryoneUserGroup: Types.UserGroup;\n    UpdateUserAdmin: Types.UpdateUserAdminResponse;\n    UpdateUserBasePermissions: Types.UpdateUserBasePermissionsResponse;\n    UpdatePermissionOnResourceType: Types.UpdatePermissionOnResourceTypeResponse;\n    UpdatePermissionOnTarget: Types.UpdatePermissionOnTargetResponse;\n    UpdateResourceMeta: Types.UpdateResourceMetaResponse;\n    CreateServer: Types.Server;\n    CopyServer: Types.Server;\n    DeleteServer: Types.Server;\n    UpdateServer: Types.Server;\n    RenameServer: Types.Update;\n    CreateNetwork: Types.Update;\n    CreateTerminal: Types.NoData;\n    DeleteTerminal: Types.NoData;\n    DeleteAllTerminals: Types.NoData;\n    CreateStack: Types.Stack;\n    CopyStack: Types.Stack;\n    DeleteStack: Types.Stack;\n    UpdateStack: Types.Stack;\n    RenameStack: Types.Update;\n    WriteStackFileContents: Types.Update;\n    RefreshStackCache: Types.NoData;\n    CreateStackWebhook: Types.CreateStackWebhookResponse;\n    DeleteStackWebhook: Types.DeleteStackWebhookResponse;\n    CreateDeployment: Types.Deployment;\n    CopyDeployment: Types.Deployment;\n    CreateDeploymentFromContainer: Types.Deployment;\n    DeleteDeployment: Types.Deployment;\n    UpdateDeployment: Types.Deployment;\n    RenameDeployment: Types.Update;\n    CreateBuild: Types.Build;\n    CopyBuild: Types.Build;\n    DeleteBuild: Types.Build;\n    UpdateBuild: Types.Build;\n    RenameBuild: Types.Update;\n    WriteBuildFileContents: Types.Update;\n    RefreshBuildCache: Types.NoData;\n    CreateBuildWebhook: Types.CreateBuildWebhookResponse;\n    DeleteBuildWebhook: Types.DeleteBuildWebhookResponse;\n    CreateBuilder: Types.Builder;\n    CopyBuilder: Types.Builder;\n    DeleteBuilder: Types.Builder;\n    UpdateBuilder: Types.Builder;\n    RenameBuilder: Types.Update;\n    CreateRepo: Types.Repo;\n    CopyRepo: Types.Repo;\n    DeleteRepo: Types.Repo;\n    UpdateRepo: Types.Repo;\n    RenameRepo: Types.Update;\n    RefreshRepoCache: Types.NoData;\n    CreateRepoWebhook: Types.CreateRepoWebhookResponse;\n    DeleteRepoWebhook: Types.DeleteRepoWebhookResponse;\n    CreateAlerter: Types.Alerter;\n    CopyAlerter: Types.Alerter;\n    DeleteAlerter: Types.Alerter;\n    UpdateAlerter: Types.Alerter;\n    RenameAlerter: Types.Update;\n    CreateProcedure: Types.Procedure;\n    CopyProcedure: Types.Procedure;\n    DeleteProcedure: Types.Procedure;\n    UpdateProcedure: Types.Procedure;\n    RenameProcedure: Types.Update;\n    CreateAction: Types.Action;\n    CopyAction: Types.Action;\n    DeleteAction: Types.Action;\n    UpdateAction: Types.Action;\n    RenameAction: Types.Update;\n    CreateResourceSync: Types.ResourceSync;\n    CopyResourceSync: Types.ResourceSync;\n    DeleteResourceSync: Types.ResourceSync;\n    UpdateResourceSync: Types.ResourceSync;\n    RenameResourceSync: Types.Update;\n    CommitSync: Types.Update;\n    WriteSyncFileContents: Types.Update;\n    RefreshResourceSyncPending: Types.ResourceSync;\n    CreateSyncWebhook: Types.CreateSyncWebhookResponse;\n    DeleteSyncWebhook: Types.DeleteSyncWebhookResponse;\n    CreateTag: Types.Tag;\n    DeleteTag: Types.Tag;\n    RenameTag: Types.Tag;\n    UpdateTagColor: Types.Tag;\n    CreateVariable: Types.CreateVariableResponse;\n    UpdateVariableValue: Types.UpdateVariableValueResponse;\n    UpdateVariableDescription: Types.UpdateVariableDescriptionResponse;\n    UpdateVariableIsSecret: Types.UpdateVariableIsSecretResponse;\n    DeleteVariable: Types.DeleteVariableResponse;\n    CreateGitProviderAccount: Types.CreateGitProviderAccountResponse;\n    UpdateGitProviderAccount: Types.UpdateGitProviderAccountResponse;\n    DeleteGitProviderAccount: Types.DeleteGitProviderAccountResponse;\n    CreateDockerRegistryAccount: Types.CreateDockerRegistryAccountResponse;\n    UpdateDockerRegistryAccount: Types.UpdateDockerRegistryAccountResponse;\n    DeleteDockerRegistryAccount: Types.DeleteDockerRegistryAccountResponse;\n};\nexport type ExecuteResponses = {\n    StartContainer: Types.Update;\n    RestartContainer: Types.Update;\n    PauseContainer: Types.Update;\n    UnpauseContainer: Types.Update;\n    StopContainer: Types.Update;\n    DestroyContainer: Types.Update;\n    StartAllContainers: Types.Update;\n    RestartAllContainers: Types.Update;\n    PauseAllContainers: Types.Update;\n    UnpauseAllContainers: Types.Update;\n    StopAllContainers: Types.Update;\n    PruneContainers: Types.Update;\n    DeleteNetwork: Types.Update;\n    PruneNetworks: Types.Update;\n    DeleteImage: Types.Update;\n    PruneImages: Types.Update;\n    DeleteVolume: Types.Update;\n    PruneVolumes: Types.Update;\n    PruneDockerBuilders: Types.Update;\n    PruneBuildx: Types.Update;\n    PruneSystem: Types.Update;\n    DeployStack: Types.Update;\n    BatchDeployStack: Types.BatchExecutionResponse;\n    DeployStackIfChanged: Types.Update;\n    BatchDeployStackIfChanged: Types.BatchExecutionResponse;\n    PullStack: Types.Update;\n    BatchPullStack: Types.BatchExecutionResponse;\n    StartStack: Types.Update;\n    RestartStack: Types.Update;\n    StopStack: Types.Update;\n    PauseStack: Types.Update;\n    UnpauseStack: Types.Update;\n    DestroyStack: Types.Update;\n    BatchDestroyStack: Types.BatchExecutionResponse;\n    Deploy: Types.Update;\n    BatchDeploy: Types.BatchExecutionResponse;\n    PullDeployment: Types.Update;\n    StartDeployment: Types.Update;\n    RestartDeployment: Types.Update;\n    PauseDeployment: Types.Update;\n    UnpauseDeployment: Types.Update;\n    StopDeployment: Types.Update;\n    DestroyDeployment: Types.Update;\n    BatchDestroyDeployment: Types.BatchExecutionResponse;\n    RunBuild: Types.Update;\n    BatchRunBuild: Types.BatchExecutionResponse;\n    CancelBuild: Types.Update;\n    CloneRepo: Types.Update;\n    BatchCloneRepo: Types.BatchExecutionResponse;\n    PullRepo: Types.Update;\n    BatchPullRepo: Types.BatchExecutionResponse;\n    BuildRepo: Types.Update;\n    BatchBuildRepo: Types.BatchExecutionResponse;\n    CancelRepoBuild: Types.Update;\n    RunProcedure: Types.Update;\n    BatchRunProcedure: Types.BatchExecutionResponse;\n    RunAction: Types.Update;\n    BatchRunAction: Types.BatchExecutionResponse;\n    RunSync: Types.Update;\n    DeployStackService: Types.Update;\n    StartStackService: Types.Update;\n    RestartStackService: Types.Update;\n    StopStackService: Types.Update;\n    PauseStackService: Types.Update;\n    UnpauseStackService: Types.Update;\n    DestroyStackService: Types.Update;\n    RunStackService: Types.Update;\n    TestAlerter: Types.Update;\n    SendAlert: Types.Update;\n    ClearRepoCache: Types.Update;\n    BackupCoreDatabase: Types.Update;\n    GlobalAutoUpdate: Types.Update;\n};\n"
  },
  {
    "path": "frontend/public/client/responses.js",
    "content": "export {};\n"
  },
  {
    "path": "frontend/public/client/terminal.d.ts",
    "content": "import { ClientState } from \"./lib\";\nimport { ConnectContainerExecQuery, ConnectDeploymentExecQuery, ConnectStackExecQuery, ConnectTerminalQuery, ExecuteContainerExecBody, ExecuteDeploymentExecBody, ExecuteStackExecBody, ExecuteTerminalBody } from \"./types\";\nexport type TerminalCallbacks = {\n    on_message?: (e: MessageEvent<any>) => void;\n    on_login?: () => void;\n    on_open?: () => void;\n    on_close?: () => void;\n};\nexport type ConnectExecQuery = {\n    type: \"container\";\n    query: ConnectContainerExecQuery;\n} | {\n    type: \"deployment\";\n    query: ConnectDeploymentExecQuery;\n} | {\n    type: \"stack\";\n    query: ConnectStackExecQuery;\n};\nexport type ExecuteExecBody = {\n    type: \"container\";\n    body: ExecuteContainerExecBody;\n} | {\n    type: \"deployment\";\n    body: ExecuteDeploymentExecBody;\n} | {\n    type: \"stack\";\n    body: ExecuteStackExecBody;\n};\nexport type ExecuteCallbacks = {\n    onLine?: (line: string) => void | Promise<void>;\n    onFinish?: (code: string) => void | Promise<void>;\n};\nexport declare const terminal_methods: (url: string, state: ClientState) => {\n    connect_terminal: ({ query, on_message, on_login, on_open, on_close, }: {\n        query: ConnectTerminalQuery;\n    } & TerminalCallbacks) => WebSocket;\n    execute_terminal: (request: ExecuteTerminalBody, callbacks?: ExecuteCallbacks) => Promise<void>;\n    execute_terminal_stream: (request: ExecuteTerminalBody) => Promise<AsyncIterable<string>>;\n    connect_exec: ({ query: { type, query }, on_message, on_login, on_open, on_close, }: {\n        query: ConnectExecQuery;\n    } & TerminalCallbacks) => WebSocket;\n    connect_container_exec: ({ query, ...callbacks }: {\n        query: ConnectContainerExecQuery;\n    } & TerminalCallbacks) => WebSocket;\n    execute_container_exec: (body: ExecuteContainerExecBody, callbacks?: ExecuteCallbacks) => Promise<void>;\n    execute_container_exec_stream: (body: ExecuteContainerExecBody) => Promise<AsyncIterable<string>>;\n    connect_deployment_exec: ({ query, ...callbacks }: {\n        query: ConnectDeploymentExecQuery;\n    } & TerminalCallbacks) => WebSocket;\n    execute_deployment_exec: (body: ExecuteDeploymentExecBody, callbacks?: ExecuteCallbacks) => Promise<void>;\n    execute_deployment_exec_stream: (body: ExecuteDeploymentExecBody) => Promise<AsyncIterable<string>>;\n    connect_stack_exec: ({ query, ...callbacks }: {\n        query: ConnectStackExecQuery;\n    } & TerminalCallbacks) => WebSocket;\n    execute_stack_exec: (body: ExecuteStackExecBody, callbacks?: ExecuteCallbacks) => Promise<void>;\n    execute_stack_exec_stream: (body: ExecuteStackExecBody) => Promise<AsyncIterable<string>>;\n};\n"
  },
  {
    "path": "frontend/public/client/terminal.js",
    "content": "export const terminal_methods = (url, state) => {\n    const connect_terminal = ({ query, on_message, on_login, on_open, on_close, }) => {\n        const url_query = new URLSearchParams(query).toString();\n        const ws = new WebSocket(url.replace(\"http\", \"ws\") + \"/ws/terminal?\" + url_query);\n        // Handle login on websocket open\n        ws.onopen = () => {\n            const login_msg = state.jwt\n                ? {\n                    type: \"Jwt\",\n                    params: {\n                        jwt: state.jwt,\n                    },\n                }\n                : {\n                    type: \"ApiKeys\",\n                    params: {\n                        key: state.key,\n                        secret: state.secret,\n                    },\n                };\n            ws.send(JSON.stringify(login_msg));\n            on_open?.();\n        };\n        ws.onmessage = (e) => {\n            if (e.data == \"LOGGED_IN\") {\n                ws.binaryType = \"arraybuffer\";\n                ws.onmessage = (e) => on_message?.(e);\n                on_login?.();\n                return;\n            }\n            else {\n                on_message?.(e);\n            }\n        };\n        ws.onclose = () => on_close?.();\n        return ws;\n    };\n    const execute_terminal = async (request, callbacks) => {\n        const stream = await execute_terminal_stream(request);\n        for await (const line of stream) {\n            if (line.startsWith(\"__KOMODO_EXIT_CODE\")) {\n                await callbacks?.onFinish?.(line.split(\":\")[1]);\n                return;\n            }\n            else {\n                await callbacks?.onLine?.(line);\n            }\n        }\n        // This is hit if no __KOMODO_EXIT_CODE is sent, ie early exit\n        await callbacks?.onFinish?.(\"Early exit without code\");\n    };\n    const execute_terminal_stream = (request) => execute_stream(\"/terminal/execute\", request);\n    const connect_container_exec = ({ query, ...callbacks }) => connect_exec({ query: { type: \"container\", query }, ...callbacks });\n    const connect_deployment_exec = ({ query, ...callbacks }) => connect_exec({ query: { type: \"deployment\", query }, ...callbacks });\n    const connect_stack_exec = ({ query, ...callbacks }) => connect_exec({ query: { type: \"stack\", query }, ...callbacks });\n    const connect_exec = ({ query: { type, query }, on_message, on_login, on_open, on_close, }) => {\n        const url_query = new URLSearchParams(query).toString();\n        const ws = new WebSocket(url.replace(\"http\", \"ws\") + `/ws/${type}/terminal?` + url_query);\n        // Handle login on websocket open\n        ws.onopen = () => {\n            const login_msg = state.jwt\n                ? {\n                    type: \"Jwt\",\n                    params: {\n                        jwt: state.jwt,\n                    },\n                }\n                : {\n                    type: \"ApiKeys\",\n                    params: {\n                        key: state.key,\n                        secret: state.secret,\n                    },\n                };\n            ws.send(JSON.stringify(login_msg));\n            on_open?.();\n        };\n        ws.onmessage = (e) => {\n            if (e.data == \"LOGGED_IN\") {\n                ws.binaryType = \"arraybuffer\";\n                ws.onmessage = (e) => on_message?.(e);\n                on_login?.();\n                return;\n            }\n            else {\n                on_message?.(e);\n            }\n        };\n        ws.onclose = () => on_close?.();\n        return ws;\n    };\n    const execute_container_exec = (body, callbacks) => execute_exec({ type: \"container\", body }, callbacks);\n    const execute_deployment_exec = (body, callbacks) => execute_exec({ type: \"deployment\", body }, callbacks);\n    const execute_stack_exec = (body, callbacks) => execute_exec({ type: \"stack\", body }, callbacks);\n    const execute_exec = async (request, callbacks) => {\n        const stream = await execute_exec_stream(request);\n        for await (const line of stream) {\n            if (line.startsWith(\"__KOMODO_EXIT_CODE\")) {\n                await callbacks?.onFinish?.(line.split(\":\")[1]);\n                return;\n            }\n            else {\n                await callbacks?.onLine?.(line);\n            }\n        }\n        // This is hit if no __KOMODO_EXIT_CODE is sent, ie early exit\n        await callbacks?.onFinish?.(\"Early exit without code\");\n    };\n    const execute_container_exec_stream = (body) => execute_exec_stream({ type: \"container\", body });\n    const execute_deployment_exec_stream = (body) => execute_exec_stream({ type: \"deployment\", body });\n    const execute_stack_exec_stream = (body) => execute_exec_stream({ type: \"stack\", body });\n    const execute_exec_stream = (request) => execute_stream(`/terminal/execute/${request.type}`, request.body);\n    const execute_stream = (path, request) => new Promise(async (res, rej) => {\n        try {\n            let response = await fetch(url + path, {\n                method: \"POST\",\n                body: JSON.stringify(request),\n                headers: {\n                    ...(state.jwt\n                        ? {\n                            authorization: state.jwt,\n                        }\n                        : state.key && state.secret\n                            ? {\n                                \"x-api-key\": state.key,\n                                \"x-api-secret\": state.secret,\n                            }\n                            : {}),\n                    \"content-type\": \"application/json\",\n                },\n            });\n            if (response.status === 200) {\n                if (response.body) {\n                    const stream = response.body\n                        .pipeThrough(new TextDecoderStream(\"utf-8\"))\n                        .pipeThrough(new TransformStream({\n                        start(_controller) {\n                            this.tail = \"\";\n                        },\n                        transform(chunk, controller) {\n                            const data = this.tail + chunk; // prepend any carry‑over\n                            const parts = data.split(/\\r?\\n/); // split on CRLF or LF\n                            this.tail = parts.pop(); // last item may be incomplete\n                            for (const line of parts)\n                                controller.enqueue(line);\n                        },\n                        flush(controller) {\n                            if (this.tail)\n                                controller.enqueue(this.tail); // final unterminated line\n                        },\n                    }));\n                    res(stream);\n                }\n                else {\n                    rej({\n                        status: response.status,\n                        result: { error: \"No response body\", trace: [] },\n                    });\n                }\n            }\n            else {\n                try {\n                    const result = await response.json();\n                    rej({ status: response.status, result });\n                }\n                catch (error) {\n                    rej({\n                        status: response.status,\n                        result: {\n                            error: \"Failed to get response body\",\n                            trace: [JSON.stringify(error)],\n                        },\n                        error,\n                    });\n                }\n            }\n        }\n        catch (error) {\n            rej({\n                status: 1,\n                result: {\n                    error: \"Request failed with error\",\n                    trace: [JSON.stringify(error)],\n                },\n                error,\n            });\n        }\n    });\n    return {\n        connect_terminal,\n        execute_terminal,\n        execute_terminal_stream,\n        connect_exec,\n        connect_container_exec,\n        execute_container_exec,\n        execute_container_exec_stream,\n        connect_deployment_exec,\n        execute_deployment_exec,\n        execute_deployment_exec_stream,\n        connect_stack_exec,\n        execute_stack_exec,\n        execute_stack_exec_stream,\n    };\n};\n"
  },
  {
    "path": "frontend/public/client/types.d.ts",
    "content": "export interface MongoIdObj {\n    $oid: string;\n}\nexport type MongoId = MongoIdObj;\n/** The levels of permission that a User or UserGroup can have on a resource. */\nexport declare enum PermissionLevel {\n    /** No permissions. */\n    None = \"None\",\n    /** Can read resource information and config */\n    Read = \"Read\",\n    /** Can execute actions on the resource */\n    Execute = \"Execute\",\n    /** Can update the resource configuration */\n    Write = \"Write\"\n}\nexport interface PermissionLevelAndSpecifics {\n    level: PermissionLevel;\n    specific: Array<SpecificPermission>;\n}\nexport type I64 = number;\nexport interface Resource<Config, Info> {\n    /**\n     * The Mongo ID of the resource.\n     * This field is de/serialized from/to JSON as\n     * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Resource<T>) }`\n     */\n    _id?: MongoId;\n    /**\n     * The resource name.\n     * This is guaranteed unique among others of the same resource type.\n     */\n    name: string;\n    /** A description for the resource */\n    description?: string;\n    /** Mark resource as a template */\n    template?: boolean;\n    /** Tag Ids */\n    tags?: string[];\n    /** Resource-specific information (not user configurable). */\n    info?: Info;\n    /** Resource-specific configuration. */\n    config?: Config;\n    /**\n     * Set a base permission level that all users will have on the\n     * resource.\n     */\n    base_permission?: PermissionLevelAndSpecifics | PermissionLevel;\n    /** When description last updated */\n    updated_at?: I64;\n}\nexport declare enum ScheduleFormat {\n    English = \"English\",\n    Cron = \"Cron\"\n}\nexport declare enum FileFormat {\n    KeyValue = \"key_value\",\n    Toml = \"toml\",\n    Yaml = \"yaml\",\n    Json = \"json\"\n}\nexport interface ActionConfig {\n    /** Whether this action should run at startup. */\n    run_at_startup: boolean;\n    /** Choose whether to specify schedule as regular CRON, or using the english to CRON parser. */\n    schedule_format?: ScheduleFormat;\n    /**\n     * Optionally provide a schedule for the procedure to run on.\n     *\n     * There are 2 ways to specify a schedule:\n     *\n     * 1. Regular CRON expression:\n     *\n     * (second, minute, hour, day, month, day-of-week)\n     * ```text\n     * 0 0 0 1,15 * ?\n     * ```\n     *\n     * 2. \"English\" expression via [english-to-cron](https://crates.io/crates/english-to-cron):\n     *\n     * ```text\n     * at midnight on the 1st and 15th of the month\n     * ```\n     */\n    schedule?: string;\n    /**\n     * Whether schedule is enabled if one is provided.\n     * Can be used to temporarily disable the schedule.\n     */\n    schedule_enabled: boolean;\n    /**\n     * Optional. A TZ Identifier. If not provided, will use Core local timezone.\n     * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n     */\n    schedule_timezone?: string;\n    /** Whether to send alerts when the schedule was run. */\n    schedule_alert: boolean;\n    /** Whether to send alerts when this action fails. */\n    failure_alert: boolean;\n    /** Whether incoming webhooks actually trigger action. */\n    webhook_enabled: boolean;\n    /**\n     * Optionally provide an alternate webhook secret for this procedure.\n     * If its an empty string, use the default secret from the config.\n     */\n    webhook_secret?: string;\n    /**\n     * Whether deno will be instructed to reload all dependencies,\n     * this can usually be kept false outside of development.\n     */\n    reload_deno_deps?: boolean;\n    /**\n     * Typescript file contents using pre-initialized `komodo` client.\n     * Supports variable / secret interpolation.\n     */\n    file_contents?: string;\n    /**\n     * Specify the format in which the arguments are defined.\n     * Default: `key_value` (like environment)\n     */\n    arguments_format?: FileFormat;\n    /** Default arguments to give to the Action for use in the script at `ARGS`. */\n    arguments?: string;\n}\n/** Represents an empty json object: `{}` */\nexport interface NoData {\n}\nexport type Action = Resource<ActionConfig, NoData>;\nexport interface ResourceListItem<Info> {\n    /** The resource id */\n    id: string;\n    /** The resource type, ie `Server` or `Deployment` */\n    type: ResourceTarget[\"type\"];\n    /** The resource name */\n    name: string;\n    /** Whether resource is a template */\n    template: boolean;\n    /** Tag Ids */\n    tags: string[];\n    /** Resource specific info */\n    info: Info;\n}\nexport declare enum ActionState {\n    /** Unknown case */\n    Unknown = \"Unknown\",\n    /** Last clone / pull successful (or never cloned) */\n    Ok = \"Ok\",\n    /** Last clone / pull failed */\n    Failed = \"Failed\",\n    /** Currently running */\n    Running = \"Running\"\n}\nexport interface ActionListItemInfo {\n    /** Whether last action run successful */\n    state: ActionState;\n    /** Action last successful run timestamp in ms. */\n    last_run_at?: I64;\n    /**\n     * If the action has schedule enabled, this is the\n     * next scheduled run time in unix ms.\n     */\n    next_scheduled_run?: I64;\n    /**\n     * If there is an error parsing schedule expression,\n     * it will be given here.\n     */\n    schedule_error?: string;\n}\nexport type ActionListItem = ResourceListItem<ActionListItemInfo>;\nexport declare enum TemplatesQueryBehavior {\n    /** Include templates in results. Default. */\n    Include = \"Include\",\n    /** Exclude templates from results. */\n    Exclude = \"Exclude\",\n    /** Results *only* includes templates. */\n    Only = \"Only\"\n}\nexport declare enum TagQueryBehavior {\n    /** Returns resources which have strictly all the tags */\n    All = \"All\",\n    /** Returns resources which have one or more of the tags */\n    Any = \"Any\"\n}\n/** Passing empty Vec is the same as not filtering by that field */\nexport interface ResourceQuery<T> {\n    names?: string[];\n    templates?: TemplatesQueryBehavior;\n    /** Pass Vec of tag ids or tag names */\n    tags?: string[];\n    /** 'All' or 'Any' */\n    tag_behavior?: TagQueryBehavior;\n    specific?: T;\n}\nexport interface ActionQuerySpecifics {\n}\nexport type ActionQuery = ResourceQuery<ActionQuerySpecifics>;\nexport type AlerterEndpoint = \n/** Send alert serialized to JSON to an http endpoint. */\n{\n    type: \"Custom\";\n    params: CustomAlerterEndpoint;\n}\n/** Send alert to a Slack app */\n | {\n    type: \"Slack\";\n    params: SlackAlerterEndpoint;\n}\n/** Send alert to a Discord app */\n | {\n    type: \"Discord\";\n    params: DiscordAlerterEndpoint;\n}\n/** Send alert to Ntfy */\n | {\n    type: \"Ntfy\";\n    params: NtfyAlerterEndpoint;\n}\n/** Send alert to Pushover */\n | {\n    type: \"Pushover\";\n    params: PushoverAlerterEndpoint;\n};\n/** Used to reference a specific resource across all resource types */\nexport type ResourceTarget = {\n    type: \"System\";\n    id: string;\n} | {\n    type: \"Server\";\n    id: string;\n} | {\n    type: \"Stack\";\n    id: string;\n} | {\n    type: \"Deployment\";\n    id: string;\n} | {\n    type: \"Build\";\n    id: string;\n} | {\n    type: \"Repo\";\n    id: string;\n} | {\n    type: \"Procedure\";\n    id: string;\n} | {\n    type: \"Action\";\n    id: string;\n} | {\n    type: \"Builder\";\n    id: string;\n} | {\n    type: \"Alerter\";\n    id: string;\n} | {\n    type: \"ResourceSync\";\n    id: string;\n};\n/** Types of maintenance schedules */\nexport declare enum MaintenanceScheduleType {\n    /** Daily at the specified time */\n    Daily = \"Daily\",\n    /** Weekly on the specified day and time */\n    Weekly = \"Weekly\",\n    /** One-time maintenance on a specific date and time */\n    OneTime = \"OneTime\"\n}\n/** Represents a scheduled maintenance window */\nexport interface MaintenanceWindow {\n    /** Name for the maintenance window (required) */\n    name: string;\n    /** Description of what maintenance is performed (optional) */\n    description?: string;\n    /**\n     * The type of maintenance schedule:\n     * - Daily (default)\n     * - Weekly\n     * - OneTime\n     */\n    schedule_type?: MaintenanceScheduleType;\n    /** For Weekly schedules: Specify the day of the week (Monday, Tuesday, etc.) */\n    day_of_week?: string;\n    /** For OneTime window: ISO 8601 date format (YYYY-MM-DD) */\n    date?: string;\n    /** Start hour in 24-hour format (0-23) (optional, defaults to 0) */\n    hour?: number;\n    /** Start minute (0-59) (optional, defaults to 0) */\n    minute?: number;\n    /** Duration of the maintenance window in minutes (required) */\n    duration_minutes: number;\n    /**\n     * Timezone for maintenance window specificiation.\n     * If empty, will use Core timezone.\n     */\n    timezone?: string;\n    /** Whether this maintenance window is currently enabled */\n    enabled: boolean;\n}\nexport interface AlerterConfig {\n    /** Whether the alerter is enabled */\n    enabled?: boolean;\n    /**\n     * Where to route the alert messages.\n     *\n     * Default: Custom endpoint `http://localhost:7000`\n     */\n    endpoint?: AlerterEndpoint;\n    /**\n     * Only send specific alert types.\n     * If empty, will send all alert types.\n     */\n    alert_types?: AlertData[\"type\"][];\n    /**\n     * Only send alerts on specific resources.\n     * If empty, will send alerts for all resources.\n     */\n    resources?: ResourceTarget[];\n    /** DON'T send alerts on these resources. */\n    except_resources?: ResourceTarget[];\n    /** Scheduled maintenance windows during which alerts will be suppressed. */\n    maintenance_windows?: MaintenanceWindow[];\n}\nexport type Alerter = Resource<AlerterConfig, undefined>;\nexport interface AlerterListItemInfo {\n    /** Whether alerter is enabled for sending alerts */\n    enabled: boolean;\n    /** The type of the alerter, eg. `Slack`, `Custom` */\n    endpoint_type: AlerterEndpoint[\"type\"];\n}\nexport type AlerterListItem = ResourceListItem<AlerterListItemInfo>;\nexport interface AlerterQuerySpecifics {\n    /**\n     * Filter alerters by enabled.\n     * - `None`: Don't filter by enabled\n     * - `Some(true)`: Only include alerts with `enabled: true`\n     * - `Some(false)`: Only include alerts with `enabled: false`\n     */\n    enabled?: boolean;\n    /**\n     * Only include alerters with these endpoint types.\n     * If empty, don't filter by enpoint type.\n     */\n    types: AlerterEndpoint[\"type\"][];\n}\nexport type AlerterQuery = ResourceQuery<AlerterQuerySpecifics>;\nexport type BatchExecutionResponseItem = {\n    status: \"Ok\";\n    data: Update;\n} | {\n    status: \"Err\";\n    data: BatchExecutionResponseItemErr;\n};\nexport type BatchExecutionResponse = BatchExecutionResponseItem[];\nexport declare enum Operation {\n    None = \"None\",\n    CreateServer = \"CreateServer\",\n    UpdateServer = \"UpdateServer\",\n    DeleteServer = \"DeleteServer\",\n    RenameServer = \"RenameServer\",\n    StartContainer = \"StartContainer\",\n    RestartContainer = \"RestartContainer\",\n    PauseContainer = \"PauseContainer\",\n    UnpauseContainer = \"UnpauseContainer\",\n    StopContainer = \"StopContainer\",\n    DestroyContainer = \"DestroyContainer\",\n    StartAllContainers = \"StartAllContainers\",\n    RestartAllContainers = \"RestartAllContainers\",\n    PauseAllContainers = \"PauseAllContainers\",\n    UnpauseAllContainers = \"UnpauseAllContainers\",\n    StopAllContainers = \"StopAllContainers\",\n    PruneContainers = \"PruneContainers\",\n    CreateNetwork = \"CreateNetwork\",\n    DeleteNetwork = \"DeleteNetwork\",\n    PruneNetworks = \"PruneNetworks\",\n    DeleteImage = \"DeleteImage\",\n    PruneImages = \"PruneImages\",\n    DeleteVolume = \"DeleteVolume\",\n    PruneVolumes = \"PruneVolumes\",\n    PruneDockerBuilders = \"PruneDockerBuilders\",\n    PruneBuildx = \"PruneBuildx\",\n    PruneSystem = \"PruneSystem\",\n    CreateStack = \"CreateStack\",\n    UpdateStack = \"UpdateStack\",\n    RenameStack = \"RenameStack\",\n    DeleteStack = \"DeleteStack\",\n    WriteStackContents = \"WriteStackContents\",\n    RefreshStackCache = \"RefreshStackCache\",\n    PullStack = \"PullStack\",\n    DeployStack = \"DeployStack\",\n    StartStack = \"StartStack\",\n    RestartStack = \"RestartStack\",\n    PauseStack = \"PauseStack\",\n    UnpauseStack = \"UnpauseStack\",\n    StopStack = \"StopStack\",\n    DestroyStack = \"DestroyStack\",\n    RunStackService = \"RunStackService\",\n    DeployStackService = \"DeployStackService\",\n    PullStackService = \"PullStackService\",\n    StartStackService = \"StartStackService\",\n    RestartStackService = \"RestartStackService\",\n    PauseStackService = \"PauseStackService\",\n    UnpauseStackService = \"UnpauseStackService\",\n    StopStackService = \"StopStackService\",\n    DestroyStackService = \"DestroyStackService\",\n    CreateDeployment = \"CreateDeployment\",\n    UpdateDeployment = \"UpdateDeployment\",\n    RenameDeployment = \"RenameDeployment\",\n    DeleteDeployment = \"DeleteDeployment\",\n    Deploy = \"Deploy\",\n    PullDeployment = \"PullDeployment\",\n    StartDeployment = \"StartDeployment\",\n    RestartDeployment = \"RestartDeployment\",\n    PauseDeployment = \"PauseDeployment\",\n    UnpauseDeployment = \"UnpauseDeployment\",\n    StopDeployment = \"StopDeployment\",\n    DestroyDeployment = \"DestroyDeployment\",\n    CreateBuild = \"CreateBuild\",\n    UpdateBuild = \"UpdateBuild\",\n    RenameBuild = \"RenameBuild\",\n    DeleteBuild = \"DeleteBuild\",\n    RunBuild = \"RunBuild\",\n    CancelBuild = \"CancelBuild\",\n    WriteDockerfile = \"WriteDockerfile\",\n    CreateRepo = \"CreateRepo\",\n    UpdateRepo = \"UpdateRepo\",\n    RenameRepo = \"RenameRepo\",\n    DeleteRepo = \"DeleteRepo\",\n    CloneRepo = \"CloneRepo\",\n    PullRepo = \"PullRepo\",\n    BuildRepo = \"BuildRepo\",\n    CancelRepoBuild = \"CancelRepoBuild\",\n    CreateProcedure = \"CreateProcedure\",\n    UpdateProcedure = \"UpdateProcedure\",\n    RenameProcedure = \"RenameProcedure\",\n    DeleteProcedure = \"DeleteProcedure\",\n    RunProcedure = \"RunProcedure\",\n    CreateAction = \"CreateAction\",\n    UpdateAction = \"UpdateAction\",\n    RenameAction = \"RenameAction\",\n    DeleteAction = \"DeleteAction\",\n    RunAction = \"RunAction\",\n    CreateBuilder = \"CreateBuilder\",\n    UpdateBuilder = \"UpdateBuilder\",\n    RenameBuilder = \"RenameBuilder\",\n    DeleteBuilder = \"DeleteBuilder\",\n    CreateAlerter = \"CreateAlerter\",\n    UpdateAlerter = \"UpdateAlerter\",\n    RenameAlerter = \"RenameAlerter\",\n    DeleteAlerter = \"DeleteAlerter\",\n    TestAlerter = \"TestAlerter\",\n    SendAlert = \"SendAlert\",\n    CreateResourceSync = \"CreateResourceSync\",\n    UpdateResourceSync = \"UpdateResourceSync\",\n    RenameResourceSync = \"RenameResourceSync\",\n    DeleteResourceSync = \"DeleteResourceSync\",\n    WriteSyncContents = \"WriteSyncContents\",\n    CommitSync = \"CommitSync\",\n    RunSync = \"RunSync\",\n    ClearRepoCache = \"ClearRepoCache\",\n    BackupCoreDatabase = \"BackupCoreDatabase\",\n    GlobalAutoUpdate = \"GlobalAutoUpdate\",\n    CreateVariable = \"CreateVariable\",\n    UpdateVariableValue = \"UpdateVariableValue\",\n    DeleteVariable = \"DeleteVariable\",\n    CreateGitProviderAccount = \"CreateGitProviderAccount\",\n    UpdateGitProviderAccount = \"UpdateGitProviderAccount\",\n    DeleteGitProviderAccount = \"DeleteGitProviderAccount\",\n    CreateDockerRegistryAccount = \"CreateDockerRegistryAccount\",\n    UpdateDockerRegistryAccount = \"UpdateDockerRegistryAccount\",\n    DeleteDockerRegistryAccount = \"DeleteDockerRegistryAccount\"\n}\n/** Represents the output of some command being run */\nexport interface Log {\n    /** A label for the log */\n    stage: string;\n    /** The command which was executed */\n    command: string;\n    /** The output of the command in the standard channel */\n    stdout: string;\n    /** The output of the command in the error channel */\n    stderr: string;\n    /** Whether the command run was successful */\n    success: boolean;\n    /** The start time of the command execution */\n    start_ts: I64;\n    /** The end time of the command execution */\n    end_ts: I64;\n}\n/** An update's status */\nexport declare enum UpdateStatus {\n    /** The run is in the system but hasn't started yet */\n    Queued = \"Queued\",\n    /** The run is currently running */\n    InProgress = \"InProgress\",\n    /** The run is complete */\n    Complete = \"Complete\"\n}\nexport interface Version {\n    major: number;\n    minor: number;\n    patch: number;\n}\n/** Represents an action performed by Komodo. */\nexport interface Update {\n    /**\n     * The Mongo ID of the update.\n     * This field is de/serialized from/to JSON as\n     * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Update) }`\n     */\n    _id?: MongoId;\n    /** The operation performed */\n    operation: Operation;\n    /** The time the operation started */\n    start_ts: I64;\n    /** Whether the operation was successful */\n    success: boolean;\n    /**\n     * The user id that triggered the update.\n     *\n     * Also can take these values for operations triggered automatically:\n     * - `Procedure`: The operation was triggered as part of a procedure run\n     * - `Github`: The operation was triggered by a github webhook\n     * - `Auto Redeploy`: The operation (always `Deploy`) was triggered by an attached build finishing.\n     */\n    operator: string;\n    /** The target resource to which this update refers */\n    target: ResourceTarget;\n    /** Logs produced as the operation is performed */\n    logs: Log[];\n    /** The time the operation completed. */\n    end_ts?: I64;\n    /**\n     * The status of the update\n     * - `Queued`\n     * - `InProgress`\n     * - `Complete`\n     */\n    status: UpdateStatus;\n    /** An optional version on the update, ie build version or deployed version. */\n    version?: Version;\n    /** An optional commit hash associated with the update, ie cloned hash or deployed hash. */\n    commit_hash?: string;\n    /** Some unstructured, operation specific data. Not for general usage. */\n    other_data?: string;\n    /** If the update is for resource config update, give the previous toml contents */\n    prev_toml?: string;\n    /** If the update is for resource config update, give the current (at time of Update) toml contents */\n    current_toml?: string;\n}\nexport type BoxUpdate = Update;\n/** Configuration for an image registry */\nexport interface ImageRegistryConfig {\n    /**\n     * Specify the registry provider domain, eg `docker.io`.\n     * If not provided, will not push to any registry.\n     */\n    domain?: string;\n    /** Specify an account to use with the registry. */\n    account?: string;\n    /**\n     * Optional. Specify an organization to push the image under.\n     * Empty string means no organization.\n     */\n    organization?: string;\n}\nexport interface SystemCommand {\n    path?: string;\n    command?: string;\n}\n/** The build configuration. */\nexport interface BuildConfig {\n    /** Which builder is used to build the image. */\n    builder_id?: string;\n    /** The current version of the build. */\n    version?: Version;\n    /**\n     * Whether to automatically increment the patch on every build.\n     * Default is `true`\n     */\n    auto_increment_version: boolean;\n    /**\n     * An alternate name for the image pushed to the repository.\n     * If this is empty, it will use the build name.\n     *\n     * Can be used in conjunction with `image_tag` to direct multiple builds\n     * with different configs to push to the same image registry, under different,\n     * independantly versioned tags.\n     */\n    image_name?: string;\n    /**\n     * An extra tag put after the build version, for the image pushed to the repository.\n     * Eg. in image tag of `aarch64` would push to moghtech/komodo-core:1.13.2-aarch64.\n     * If this is empty, the image tag will just be the build version.\n     *\n     * Can be used in conjunction with `image_name` to direct multiple builds\n     * with different configs to push to the same image registry, under different,\n     * independantly versioned tags.\n     */\n    image_tag?: string;\n    /** Push `:latest` / `:latest-image_tag` tags. */\n    include_latest_tag: boolean;\n    /** Push build version semver `:1.19.5` + `1.19` / `:1.19.5-image_tag` tags. */\n    include_version_tags: boolean;\n    /** Push commit hash `:a6v8h83` / `:a6v8h83-image_tag` tags. */\n    include_commit_tag: boolean;\n    /** Configure quick links that are displayed in the resource header */\n    links?: string[];\n    /** Choose a Komodo Repo (Resource) to source the build files. */\n    linked_repo?: string;\n    /** The git provider domain. Default: github.com */\n    git_provider: string;\n    /**\n     * Whether to use https to clone the repo (versus http). Default: true\n     *\n     * Note. Komodo does not currently support cloning repos via ssh.\n     */\n    git_https: boolean;\n    /**\n     * The git account used to access private repos.\n     * Passing empty string can only clone public repos.\n     *\n     * Note. A token for the account must be available in the core config or the builder server's periphery config\n     * for the configured git provider.\n     */\n    git_account?: string;\n    /** The repo used as the source of the build. */\n    repo?: string;\n    /** The branch of the repo. */\n    branch: string;\n    /** Optionally set a specific commit hash. */\n    commit?: string;\n    /** Whether incoming webhooks actually trigger action. */\n    webhook_enabled: boolean;\n    /**\n     * Optionally provide an alternate webhook secret for this build.\n     * If its an empty string, use the default secret from the config.\n     */\n    webhook_secret?: string;\n    /**\n     * If this is checked, the build will source the files on the host.\n     * Use `build_path` and `dockerfile_path` to specify the path on the host.\n     * This is useful for those who wish to setup their files on the host,\n     * rather than defining the contents in UI or in a git repo.\n     */\n    files_on_host?: boolean;\n    /**\n     * The path of the docker build context relative to the root of the repo.\n     * Default: \".\" (the root of the repo).\n     */\n    build_path: string;\n    /** The path of the dockerfile relative to the build path. */\n    dockerfile_path: string;\n    /**\n     * Configuration for the registry/s to push the built image to.\n     * The first registry in this list will be used with attached Deployments.\n     */\n    image_registry?: ImageRegistryConfig[];\n    /** Whether to skip secret interpolation in the build_args. */\n    skip_secret_interp?: boolean;\n    /** Whether to use buildx to build (eg `docker buildx build ...`) */\n    use_buildx?: boolean;\n    /** Any extra docker cli arguments to be included in the build command */\n    extra_args?: string[];\n    /** The optional command run after repo clone and before docker build. */\n    pre_build?: SystemCommand;\n    /**\n     * UI defined dockerfile contents.\n     * Supports variable / secret interpolation.\n     */\n    dockerfile?: string;\n    /**\n     * Docker build arguments.\n     *\n     * These values are visible in the final image by running `docker inspect`.\n     */\n    build_args?: string;\n    /**\n     * Secret arguments.\n     *\n     * These values remain hidden in the final image by using\n     * docker secret mounts. See <https://docs.docker.com/build/building/secrets>.\n     *\n     * The values can be used in RUN commands:\n     * ```sh\n     * RUN --mount=type=secret,id=SECRET_KEY \\\n     * SECRET_KEY=$(cat /run/secrets/SECRET_KEY) ...\n     * ```\n     */\n    secret_args?: string;\n    /** Docker labels */\n    labels?: string;\n}\nexport interface BuildInfo {\n    /** The timestamp build was last built. */\n    last_built_at: I64;\n    /** Latest built short commit hash, or null. */\n    built_hash?: string;\n    /** Latest built commit message, or null. Only for repo based stacks */\n    built_message?: string;\n    /**\n     * The last built dockerfile contents.\n     * This is updated whenever Komodo successfully runs the build.\n     */\n    built_contents?: string;\n    /** The absolute path to the file */\n    remote_path?: string;\n    /**\n     * The remote dockerfile contents, whether on host or in repo.\n     * This is updated whenever Komodo refreshes the build cache.\n     * It will be empty if the dockerfile is defined directly in the build config.\n     */\n    remote_contents?: string;\n    /** If there was an error in getting the remote contents, it will be here. */\n    remote_error?: string;\n    /** Latest remote short commit hash, or null. */\n    latest_hash?: string;\n    /** Latest remote commit message, or null */\n    latest_message?: string;\n}\nexport type Build = Resource<BuildConfig, BuildInfo>;\nexport declare enum BuildState {\n    /** Currently building */\n    Building = \"Building\",\n    /** Last build successful (or never built) */\n    Ok = \"Ok\",\n    /** Last build failed */\n    Failed = \"Failed\",\n    /** Other case */\n    Unknown = \"Unknown\"\n}\nexport interface BuildListItemInfo {\n    /** State of the build. Reflects whether most recent build successful. */\n    state: BuildState;\n    /** Unix timestamp in milliseconds of last build */\n    last_built_at: I64;\n    /** The current version of the build */\n    version: Version;\n    /** The builder attached to build. */\n    builder_id: string;\n    /** Whether build is in files on host mode. */\n    files_on_host: boolean;\n    /** Whether build has UI defined dockerfile contents */\n    dockerfile_contents: boolean;\n    /** Linked repo, if one is attached. */\n    linked_repo: string;\n    /** The git provider domain */\n    git_provider: string;\n    /** The repo used as the source of the build */\n    repo: string;\n    /** The branch of the repo */\n    branch: string;\n    /** Full link to the repo. */\n    repo_link: string;\n    /** Latest built short commit hash, or null. */\n    built_hash?: string;\n    /** Latest short commit hash, or null. Only for repo based stacks */\n    latest_hash?: string;\n    /** The first listed image registry domain */\n    image_registry_domain?: string;\n}\nexport type BuildListItem = ResourceListItem<BuildListItemInfo>;\nexport interface BuildQuerySpecifics {\n    builder_ids?: string[];\n    repos?: string[];\n    /**\n     * query for builds last built more recently than this timestamp\n     * defaults to 0 which is a no op\n     */\n    built_since?: I64;\n}\nexport type BuildQuery = ResourceQuery<BuildQuerySpecifics>;\nexport type BuilderConfig = \n/** Use a Periphery address as a Builder. */\n{\n    type: \"Url\";\n    params: UrlBuilderConfig;\n}\n/** Use a connected server as a Builder. */\n | {\n    type: \"Server\";\n    params: ServerBuilderConfig;\n}\n/** Use EC2 instances spawned on demand as a Builder. */\n | {\n    type: \"Aws\";\n    params: AwsBuilderConfig;\n};\nexport type Builder = Resource<BuilderConfig, undefined>;\nexport interface BuilderListItemInfo {\n    /** 'Url', 'Server', or 'Aws' */\n    builder_type: string;\n    /**\n     * If 'Url': null\n     * If 'Server': the server id\n     * If 'Aws': the instance type (eg. c5.xlarge)\n     */\n    instance_type?: string;\n}\nexport type BuilderListItem = ResourceListItem<BuilderListItemInfo>;\nexport interface BuilderQuerySpecifics {\n}\nexport type BuilderQuery = ResourceQuery<BuilderQuerySpecifics>;\n/** A wrapper for all Komodo exections. */\nexport type Execution = \n/** The \"null\" execution. Does nothing. */\n{\n    type: \"None\";\n    params: NoData;\n}\n/** Run the target action. (alias: `action`, `ac`) */\n | {\n    type: \"RunAction\";\n    params: RunAction;\n} | {\n    type: \"BatchRunAction\";\n    params: BatchRunAction;\n}\n/** Run the target procedure. (alias: `procedure`, `pr`) */\n | {\n    type: \"RunProcedure\";\n    params: RunProcedure;\n} | {\n    type: \"BatchRunProcedure\";\n    params: BatchRunProcedure;\n}\n/** Run the target build. (alias: `build`, `bd`) */\n | {\n    type: \"RunBuild\";\n    params: RunBuild;\n} | {\n    type: \"BatchRunBuild\";\n    params: BatchRunBuild;\n} | {\n    type: \"CancelBuild\";\n    params: CancelBuild;\n}\n/** Deploy the target deployment. (alias: `dp`) */\n | {\n    type: \"Deploy\";\n    params: Deploy;\n} | {\n    type: \"BatchDeploy\";\n    params: BatchDeploy;\n} | {\n    type: \"PullDeployment\";\n    params: PullDeployment;\n} | {\n    type: \"StartDeployment\";\n    params: StartDeployment;\n} | {\n    type: \"RestartDeployment\";\n    params: RestartDeployment;\n} | {\n    type: \"PauseDeployment\";\n    params: PauseDeployment;\n} | {\n    type: \"UnpauseDeployment\";\n    params: UnpauseDeployment;\n} | {\n    type: \"StopDeployment\";\n    params: StopDeployment;\n} | {\n    type: \"DestroyDeployment\";\n    params: DestroyDeployment;\n} | {\n    type: \"BatchDestroyDeployment\";\n    params: BatchDestroyDeployment;\n}\n/** Clone the target repo */\n | {\n    type: \"CloneRepo\";\n    params: CloneRepo;\n} | {\n    type: \"BatchCloneRepo\";\n    params: BatchCloneRepo;\n} | {\n    type: \"PullRepo\";\n    params: PullRepo;\n} | {\n    type: \"BatchPullRepo\";\n    params: BatchPullRepo;\n} | {\n    type: \"BuildRepo\";\n    params: BuildRepo;\n} | {\n    type: \"BatchBuildRepo\";\n    params: BatchBuildRepo;\n} | {\n    type: \"CancelRepoBuild\";\n    params: CancelRepoBuild;\n} | {\n    type: \"StartContainer\";\n    params: StartContainer;\n} | {\n    type: \"RestartContainer\";\n    params: RestartContainer;\n} | {\n    type: \"PauseContainer\";\n    params: PauseContainer;\n} | {\n    type: \"UnpauseContainer\";\n    params: UnpauseContainer;\n} | {\n    type: \"StopContainer\";\n    params: StopContainer;\n} | {\n    type: \"DestroyContainer\";\n    params: DestroyContainer;\n} | {\n    type: \"StartAllContainers\";\n    params: StartAllContainers;\n} | {\n    type: \"RestartAllContainers\";\n    params: RestartAllContainers;\n} | {\n    type: \"PauseAllContainers\";\n    params: PauseAllContainers;\n} | {\n    type: \"UnpauseAllContainers\";\n    params: UnpauseAllContainers;\n} | {\n    type: \"StopAllContainers\";\n    params: StopAllContainers;\n} | {\n    type: \"PruneContainers\";\n    params: PruneContainers;\n} | {\n    type: \"DeleteNetwork\";\n    params: DeleteNetwork;\n} | {\n    type: \"PruneNetworks\";\n    params: PruneNetworks;\n} | {\n    type: \"DeleteImage\";\n    params: DeleteImage;\n} | {\n    type: \"PruneImages\";\n    params: PruneImages;\n} | {\n    type: \"DeleteVolume\";\n    params: DeleteVolume;\n} | {\n    type: \"PruneVolumes\";\n    params: PruneVolumes;\n} | {\n    type: \"PruneDockerBuilders\";\n    params: PruneDockerBuilders;\n} | {\n    type: \"PruneBuildx\";\n    params: PruneBuildx;\n} | {\n    type: \"PruneSystem\";\n    params: PruneSystem;\n}\n/** Execute a Resource Sync. (alias: `sync`) */\n | {\n    type: \"RunSync\";\n    params: RunSync;\n}\n/** Commit a Resource Sync. (alias: `commit`) */\n | {\n    type: \"CommitSync\";\n    params: CommitSync;\n}\n/** Deploy the target stack. (alias: `stack`, `st`) */\n | {\n    type: \"DeployStack\";\n    params: DeployStack;\n} | {\n    type: \"BatchDeployStack\";\n    params: BatchDeployStack;\n} | {\n    type: \"DeployStackIfChanged\";\n    params: DeployStackIfChanged;\n} | {\n    type: \"BatchDeployStackIfChanged\";\n    params: BatchDeployStackIfChanged;\n} | {\n    type: \"PullStack\";\n    params: PullStack;\n} | {\n    type: \"BatchPullStack\";\n    params: BatchPullStack;\n} | {\n    type: \"StartStack\";\n    params: StartStack;\n} | {\n    type: \"RestartStack\";\n    params: RestartStack;\n} | {\n    type: \"PauseStack\";\n    params: PauseStack;\n} | {\n    type: \"UnpauseStack\";\n    params: UnpauseStack;\n} | {\n    type: \"StopStack\";\n    params: StopStack;\n} | {\n    type: \"DestroyStack\";\n    params: DestroyStack;\n} | {\n    type: \"BatchDestroyStack\";\n    params: BatchDestroyStack;\n} | {\n    type: \"RunStackService\";\n    params: RunStackService;\n} | {\n    type: \"TestAlerter\";\n    params: TestAlerter;\n} | {\n    type: \"SendAlert\";\n    params: SendAlert;\n} | {\n    type: \"ClearRepoCache\";\n    params: ClearRepoCache;\n} | {\n    type: \"BackupCoreDatabase\";\n    params: BackupCoreDatabase;\n} | {\n    type: \"GlobalAutoUpdate\";\n    params: GlobalAutoUpdate;\n} | {\n    type: \"Sleep\";\n    params: Sleep;\n};\n/** Allows to enable / disabled procedures in the sequence / parallel vec on the fly */\nexport interface EnabledExecution {\n    /** The execution request to run. */\n    execution: Execution;\n    /** Whether the execution is enabled to run in the procedure. */\n    enabled: boolean;\n}\n/** A single stage of a procedure. Runs a list of executions in parallel. */\nexport interface ProcedureStage {\n    /** A name for the procedure */\n    name: string;\n    /** Whether the stage should be run as part of the procedure. */\n    enabled: boolean;\n    /** The executions in the stage */\n    executions?: EnabledExecution[];\n}\n/** Config for the [Procedure] */\nexport interface ProcedureConfig {\n    /** The stages to be run by the procedure. */\n    stages?: ProcedureStage[];\n    /** Choose whether to specify schedule as regular CRON, or using the english to CRON parser. */\n    schedule_format?: ScheduleFormat;\n    /**\n     * Optionally provide a schedule for the procedure to run on.\n     *\n     * There are 2 ways to specify a schedule:\n     *\n     * 1. Regular CRON expression:\n     *\n     * (second, minute, hour, day, month, day-of-week)\n     * ```text\n     * 0 0 0 1,15 * ?\n     * ```\n     *\n     * 2. \"English\" expression via [english-to-cron](https://crates.io/crates/english-to-cron):\n     *\n     * ```text\n     * at midnight on the 1st and 15th of the month\n     * ```\n     */\n    schedule?: string;\n    /**\n     * Whether schedule is enabled if one is provided.\n     * Can be used to temporarily disable the schedule.\n     */\n    schedule_enabled: boolean;\n    /**\n     * Optional. A TZ Identifier. If not provided, will use Core local timezone.\n     * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n     */\n    schedule_timezone?: string;\n    /** Whether to send alerts when the schedule was run. */\n    schedule_alert: boolean;\n    /** Whether to send alerts when this procedure fails. */\n    failure_alert: boolean;\n    /** Whether incoming webhooks actually trigger action. */\n    webhook_enabled: boolean;\n    /**\n     * Optionally provide an alternate webhook secret for this procedure.\n     * If its an empty string, use the default secret from the config.\n     */\n    webhook_secret?: string;\n}\n/**\n * Procedures run a series of stages sequentially, where\n * each stage runs executions in parallel.\n */\nexport type Procedure = Resource<ProcedureConfig, undefined>;\nexport type CopyProcedureResponse = Procedure;\nexport type CreateActionWebhookResponse = NoData;\n/** Response for [CreateApiKey]. */\nexport interface CreateApiKeyResponse {\n    /** X-API-KEY */\n    key: string;\n    /**\n     * X-API-SECRET\n     *\n     * Note.\n     * There is no way to get the secret again after it is distributed in this message\n     */\n    secret: string;\n}\nexport type CreateApiKeyForServiceUserResponse = CreateApiKeyResponse;\nexport type CreateBuildWebhookResponse = NoData;\n/** Configuration to access private image repositories on various registries. */\nexport interface DockerRegistryAccount {\n    /**\n     * The Mongo ID of the docker registry account.\n     * This field is de/serialized from/to JSON as\n     * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of DockerRegistryAccount) }`\n     */\n    _id?: MongoId;\n    /**\n     * The domain of the provider.\n     *\n     * For docker registry, this can include 'http://...',\n     * however this is not recommended and won't work unless \"insecure registries\" are enabled\n     * on your hosts. See <https://docs.docker.com/reference/cli/dockerd/#insecure-registries>.\n     */\n    domain: string;\n    /** The account username */\n    username?: string;\n    /**\n     * The token in plain text on the db.\n     * If the database / host can be accessed this is insecure.\n     */\n    token?: string;\n}\nexport type CreateDockerRegistryAccountResponse = DockerRegistryAccount;\n/**\n * Configuration to access private git repos from various git providers.\n * Note. Cannot create two accounts with the same domain and username.\n */\nexport interface GitProviderAccount {\n    /**\n     * The Mongo ID of the git provider account.\n     * This field is de/serialized from/to JSON as\n     * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized User) }`\n     */\n    _id?: MongoId;\n    /**\n     * The domain of the provider.\n     *\n     * For git, this cannot include the protocol eg 'http://',\n     * which is controlled with 'https' field.\n     */\n    domain: string;\n    /** Whether git provider is accessed over http or https. */\n    https: boolean;\n    /** The account username */\n    username?: string;\n    /**\n     * The token in plain text on the db.\n     * If the database / host can be accessed this is insecure.\n     */\n    token?: string;\n}\nexport type CreateGitProviderAccountResponse = GitProviderAccount;\nexport type UserConfig = \n/** User that logs in with username / password */\n{\n    type: \"Local\";\n    data: {\n        password: string;\n    };\n}\n/** User that logs in via Google Oauth */\n | {\n    type: \"Google\";\n    data: {\n        google_id: string;\n        avatar: string;\n    };\n}\n/** User that logs in via Github Oauth */\n | {\n    type: \"Github\";\n    data: {\n        github_id: string;\n        avatar: string;\n    };\n}\n/** User that logs in via Oidc provider */\n | {\n    type: \"Oidc\";\n    data: {\n        provider: string;\n        user_id: string;\n    };\n}\n/** Non-human managed user, can have it's own permissions / api keys */\n | {\n    type: \"Service\";\n    data: {\n        description: string;\n    };\n};\nexport interface User {\n    /**\n     * The Mongo ID of the User.\n     * This field is de/serialized from/to JSON as\n     * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of User schema) }`\n     */\n    _id?: MongoId;\n    /** The globally unique username for the user. */\n    username: string;\n    /** Whether user is enabled / able to access the api. */\n    enabled?: boolean;\n    /** Can give / take other users admin priviledges. */\n    super_admin?: boolean;\n    /** Whether the user has global admin permissions. */\n    admin?: boolean;\n    /** Whether the user has permission to create servers. */\n    create_server_permissions?: boolean;\n    /** Whether the user has permission to create builds */\n    create_build_permissions?: boolean;\n    /** The user-type specific config. */\n    config: UserConfig;\n    /** When the user last opened updates dropdown. */\n    last_update_view?: I64;\n    /** Recently viewed ids */\n    recents?: Record<ResourceTarget[\"type\"], string[]>;\n    /** Give the user elevated permissions on all resources of a certain type */\n    all?: Record<ResourceTarget[\"type\"], PermissionLevelAndSpecifics | PermissionLevel>;\n    updated_at?: I64;\n}\nexport type CreateLocalUserResponse = User;\nexport type CreateProcedureResponse = Procedure;\nexport type CreateRepoWebhookResponse = NoData;\nexport type CreateServiceUserResponse = User;\nexport type CreateStackWebhookResponse = NoData;\nexport type CreateSyncWebhookResponse = NoData;\n/**\n * A non-secret global variable which can be interpolated into deployment\n * environment variable values and build argument values.\n */\nexport interface Variable {\n    /**\n     * Unique name associated with the variable.\n     * Instances of '[[variable.name]]' in value will be replaced with 'variable.value'.\n     */\n    name: string;\n    /** A description for the variable. */\n    description?: string;\n    /** The value associated with the variable. */\n    value?: string;\n    /**\n     * If marked as secret, the variable value will be hidden in updates / logs.\n     * Additionally the value will not be served in read requests by non admin users.\n     *\n     * Note that the value is NOT encrypted in the database, and will likely show up in database logs.\n     * The security of these variables comes down to the security\n     * of the database (system level encryption, network isolation, etc.)\n     */\n    is_secret?: boolean;\n}\nexport type CreateVariableResponse = Variable;\nexport type DeleteActionWebhookResponse = NoData;\nexport type DeleteApiKeyForServiceUserResponse = NoData;\nexport type DeleteApiKeyResponse = NoData;\nexport type DeleteBuildWebhookResponse = NoData;\nexport type DeleteDockerRegistryAccountResponse = DockerRegistryAccount;\nexport type DeleteGitProviderAccountResponse = GitProviderAccount;\nexport type DeleteProcedureResponse = Procedure;\nexport type DeleteRepoWebhookResponse = NoData;\nexport type DeleteStackWebhookResponse = NoData;\nexport type DeleteSyncWebhookResponse = NoData;\nexport type DeleteUserResponse = User;\nexport type DeleteVariableResponse = Variable;\nexport type DeploymentImage = \n/** Deploy any external image. */\n{\n    type: \"Image\";\n    params: {\n        /** The docker image, can be from any registry that works with docker and that the host server can reach. */\n        image?: string;\n    };\n}\n/** Deploy a Komodo Build. */\n | {\n    type: \"Build\";\n    params: {\n        /** The id of the Build */\n        build_id?: string;\n        /**\n         * Use a custom / older version of the image produced by the build.\n         * if version is 0.0.0, this means `latest` image.\n         */\n        version?: Version;\n    };\n};\nexport declare enum RestartMode {\n    NoRestart = \"no\",\n    OnFailure = \"on-failure\",\n    Always = \"always\",\n    UnlessStopped = \"unless-stopped\"\n}\nexport declare enum TerminationSignal {\n    SigHup = \"SIGHUP\",\n    SigInt = \"SIGINT\",\n    SigQuit = \"SIGQUIT\",\n    SigTerm = \"SIGTERM\"\n}\nexport interface DeploymentConfig {\n    /** The id of server the deployment is deployed on. */\n    server_id?: string;\n    /**\n     * The image which the deployment deploys.\n     * Can either be a user inputted image, or a Komodo Build.\n     */\n    image?: DeploymentImage;\n    /**\n     * Configure the account used to pull the image from the registry.\n     * Used with `docker login`.\n     *\n     * - If the field is empty string, will use the same account config as the build, or none at all if using image.\n     * - If the field contains an account, a token for the account must be available.\n     * - Will get the registry domain from the build / image\n     */\n    image_registry_account?: string;\n    /** Whether to skip secret interpolation into the deployment environment variables. */\n    skip_secret_interp?: boolean;\n    /** Whether to redeploy the deployment whenever the attached build finishes. */\n    redeploy_on_build?: boolean;\n    /** Whether to poll for any updates to the image. */\n    poll_for_updates?: boolean;\n    /**\n     * Whether to automatically redeploy when\n     * newer a image is found. Will implicitly\n     * enable `poll_for_updates`, you don't need to\n     * enable both.\n     */\n    auto_update?: boolean;\n    /** Whether to send ContainerStateChange alerts for this deployment. */\n    send_alerts: boolean;\n    /** Configure quick links that are displayed in the resource header */\n    links?: string[];\n    /**\n     * The network attached to the container.\n     * Default is `host`.\n     */\n    network: string;\n    /** The restart mode given to the container. */\n    restart?: RestartMode;\n    /**\n     * This is interpolated at the end of the `docker run` command,\n     * which means they are either passed to the containers inner process,\n     * or replaces the container command, depending on use of ENTRYPOINT or CMD in dockerfile.\n     * Empty is no command.\n     */\n    command?: string;\n    /** The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal). */\n    termination_signal?: TerminationSignal;\n    /** The termination timeout. */\n    termination_timeout: number;\n    /**\n     * Extra args which are interpolated into the `docker run` command,\n     * and affect the container configuration.\n     */\n    extra_args?: string[];\n    /**\n     * Labels attached to various termination signal options.\n     * Used to specify different shutdown functionality depending on the termination signal.\n     */\n    term_signal_labels?: string;\n    /**\n     * The container port mapping.\n     * Irrelevant if container network is `host`.\n     * Maps ports on host to ports on container.\n     */\n    ports?: string;\n    /**\n     * The container volume mapping.\n     * Maps files / folders on host to files / folders in container.\n     */\n    volumes?: string;\n    /** The environment variables passed to the container. */\n    environment?: string;\n    /** The docker labels given to the container. */\n    labels?: string;\n}\nexport type Deployment = Resource<DeploymentConfig, undefined>;\n/**\n * Variants de/serialized from/to snake_case.\n *\n * Eg.\n * - NotDeployed -> not_deployed\n * - Restarting -> restarting\n * - Running -> running.\n */\nexport declare enum DeploymentState {\n    /** The deployment is currently re/deploying */\n    Deploying = \"deploying\",\n    /** Container is running */\n    Running = \"running\",\n    /** Container is created but not running */\n    Created = \"created\",\n    /** Container is in restart loop */\n    Restarting = \"restarting\",\n    /** Container is being removed */\n    Removing = \"removing\",\n    /** Container is paused */\n    Paused = \"paused\",\n    /** Container is exited */\n    Exited = \"exited\",\n    /** Container is dead */\n    Dead = \"dead\",\n    /** The deployment is not deployed (no matching container) */\n    NotDeployed = \"not_deployed\",\n    /** Server not reachable for status */\n    Unknown = \"unknown\"\n}\nexport interface DeploymentListItemInfo {\n    /** The state of the deployment / underlying docker container. */\n    state: DeploymentState;\n    /** The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) */\n    status?: string;\n    /** The image attached to the deployment. */\n    image: string;\n    /** Whether there is a newer image available at the same tag. */\n    update_available: boolean;\n    /** The server that deployment sits on. */\n    server_id: string;\n    /** An attached Komodo Build, if it exists. */\n    build_id?: string;\n}\nexport type DeploymentListItem = ResourceListItem<DeploymentListItemInfo>;\nexport interface DeploymentQuerySpecifics {\n    /**\n     * Query only for Deployments on these Servers.\n     * If empty, does not filter by Server.\n     * Only accepts Server id (not name).\n     */\n    server_ids?: string[];\n    /**\n     * Query only for Deployments with these Builds attached.\n     * If empty, does not filter by Build.\n     * Only accepts Build id (not name).\n     */\n    build_ids?: string[];\n    /** Query only for Deployments with available image updates. */\n    update_available?: boolean;\n}\nexport type DeploymentQuery = ResourceQuery<DeploymentQuerySpecifics>;\n/** JSON containing an authentication token. */\nexport interface JwtResponse {\n    /** User ID for signed in user. */\n    user_id: string;\n    /** A token the user can use to authenticate their requests. */\n    jwt: string;\n}\n/** Response for [ExchangeForJwt]. */\nexport type ExchangeForJwtResponse = JwtResponse;\n/** Response containing pretty formatted toml contents. */\nexport interface TomlResponse {\n    toml: string;\n}\nexport type ExportAllResourcesToTomlResponse = TomlResponse;\nexport type ExportResourcesToTomlResponse = TomlResponse;\nexport type FindUserResponse = User;\nexport interface ActionActionState {\n    /** Number of instances of the Action currently running */\n    running: number;\n}\nexport type GetActionActionStateResponse = ActionActionState;\nexport type GetActionResponse = Action;\n/** Severity level of problem. */\nexport declare enum SeverityLevel {\n    /**\n     * No problem.\n     *\n     * Aliases: ok, low, l\n     */\n    Ok = \"OK\",\n    /**\n     * Problem is imminent.\n     *\n     * Aliases: warning, w, medium, m\n     */\n    Warning = \"WARNING\",\n    /**\n     * Problem fully realized.\n     *\n     * Aliases: critical, c, high, h\n     */\n    Critical = \"CRITICAL\"\n}\n/** The variants of data related to the alert. */\nexport type AlertData = \n/** A null alert */\n{\n    type: \"None\";\n    data: {};\n}\n/**\n * The user triggered a test of the\n * Alerter configuration.\n */\n | {\n    type: \"Test\";\n    data: {\n        /** The id of the alerter */\n        id: string;\n        /** The name of the alerter */\n        name: string;\n    };\n}\n/** A server could not be reached. */\n | {\n    type: \"ServerUnreachable\";\n    data: {\n        /** The id of the server */\n        id: string;\n        /** The name of the server */\n        name: string;\n        /** The region of the server */\n        region?: string;\n        /** The error data */\n        err?: _Serror;\n    };\n}\n/** A server has high CPU usage. */\n | {\n    type: \"ServerCpu\";\n    data: {\n        /** The id of the server */\n        id: string;\n        /** The name of the server */\n        name: string;\n        /** The region of the server */\n        region?: string;\n        /** The cpu usage percentage */\n        percentage: number;\n    };\n}\n/** A server has high memory usage. */\n | {\n    type: \"ServerMem\";\n    data: {\n        /** The id of the server */\n        id: string;\n        /** The name of the server */\n        name: string;\n        /** The region of the server */\n        region?: string;\n        /** The used memory */\n        used_gb: number;\n        /** The total memory */\n        total_gb: number;\n    };\n}\n/** A server has high disk usage. */\n | {\n    type: \"ServerDisk\";\n    data: {\n        /** The id of the server */\n        id: string;\n        /** The name of the server */\n        name: string;\n        /** The region of the server */\n        region?: string;\n        /** The mount path of the disk */\n        path: string;\n        /** The used portion of the disk in GB */\n        used_gb: number;\n        /** The total size of the disk in GB */\n        total_gb: number;\n    };\n}\n/** A server has a version mismatch with the core. */\n | {\n    type: \"ServerVersionMismatch\";\n    data: {\n        /** The id of the server */\n        id: string;\n        /** The name of the server */\n        name: string;\n        /** The region of the server */\n        region?: string;\n        /** The actual server version */\n        server_version: string;\n        /** The core version */\n        core_version: string;\n    };\n}\n/** A container's state has changed unexpectedly. */\n | {\n    type: \"ContainerStateChange\";\n    data: {\n        /** The id of the deployment */\n        id: string;\n        /** The name of the deployment */\n        name: string;\n        /** The server id of server that the deployment is on */\n        server_id: string;\n        /** The server name */\n        server_name: string;\n        /** The previous container state */\n        from: DeploymentState;\n        /** The current container state */\n        to: DeploymentState;\n    };\n}\n/** A Deployment has an image update available */\n | {\n    type: \"DeploymentImageUpdateAvailable\";\n    data: {\n        /** The id of the deployment */\n        id: string;\n        /** The name of the deployment */\n        name: string;\n        /** The server id of server that the deployment is on */\n        server_id: string;\n        /** The server name */\n        server_name: string;\n        /** The image with update */\n        image: string;\n    };\n}\n/** A Deployment has an image update available */\n | {\n    type: \"DeploymentAutoUpdated\";\n    data: {\n        /** The id of the deployment */\n        id: string;\n        /** The name of the deployment */\n        name: string;\n        /** The server id of server that the deployment is on */\n        server_id: string;\n        /** The server name */\n        server_name: string;\n        /** The updated image */\n        image: string;\n    };\n}\n/** A stack's state has changed unexpectedly. */\n | {\n    type: \"StackStateChange\";\n    data: {\n        /** The id of the stack */\n        id: string;\n        /** The name of the stack */\n        name: string;\n        /** The server id of server that the stack is on */\n        server_id: string;\n        /** The server name */\n        server_name: string;\n        /** The previous stack state */\n        from: StackState;\n        /** The current stack state */\n        to: StackState;\n    };\n}\n/** A Stack has an image update available */\n | {\n    type: \"StackImageUpdateAvailable\";\n    data: {\n        /** The id of the stack */\n        id: string;\n        /** The name of the stack */\n        name: string;\n        /** The server id of server that the stack is on */\n        server_id: string;\n        /** The server name */\n        server_name: string;\n        /** The service name to update */\n        service: string;\n        /** The image with update */\n        image: string;\n    };\n}\n/** A Stack was auto updated */\n | {\n    type: \"StackAutoUpdated\";\n    data: {\n        /** The id of the stack */\n        id: string;\n        /** The name of the stack */\n        name: string;\n        /** The server id of server that the stack is on */\n        server_id: string;\n        /** The server name */\n        server_name: string;\n        /** One or more images that were updated */\n        images: string[];\n    };\n}\n/** An AWS builder failed to terminate. */\n | {\n    type: \"AwsBuilderTerminationFailed\";\n    data: {\n        /** The id of the aws instance which failed to terminate */\n        instance_id: string;\n        /** A reason for the failure */\n        message: string;\n    };\n}\n/** A resource sync has pending updates */\n | {\n    type: \"ResourceSyncPendingUpdates\";\n    data: {\n        /** The id of the resource sync */\n        id: string;\n        /** The name of the resource sync */\n        name: string;\n    };\n}\n/** A build has failed */\n | {\n    type: \"BuildFailed\";\n    data: {\n        /** The id of the build */\n        id: string;\n        /** The name of the build */\n        name: string;\n        /** The version that failed to build */\n        version: Version;\n    };\n}\n/** A repo has failed */\n | {\n    type: \"RepoBuildFailed\";\n    data: {\n        /** The id of the repo */\n        id: string;\n        /** The name of the repo */\n        name: string;\n    };\n}\n/** A procedure has failed */\n | {\n    type: \"ProcedureFailed\";\n    data: {\n        /** The id of the procedure */\n        id: string;\n        /** The name of the procedure */\n        name: string;\n    };\n}\n/** An action has failed */\n | {\n    type: \"ActionFailed\";\n    data: {\n        /** The id of the action */\n        id: string;\n        /** The name of the action */\n        name: string;\n    };\n}\n/** A schedule was run */\n | {\n    type: \"ScheduleRun\";\n    data: {\n        /** Procedure or Action */\n        resource_type: ResourceTarget[\"type\"];\n        /** The resource id */\n        id: string;\n        /** The resource name */\n        name: string;\n    };\n}\n/**\n * Custom header / body.\n * Produced using `/execute/SendAlert`\n */\n | {\n    type: \"Custom\";\n    data: {\n        /** The alert message. */\n        message: string;\n        /** Message details. May be empty string. */\n        details?: string;\n    };\n};\n/** Representation of an alert in the system. */\nexport interface Alert {\n    /**\n     * The Mongo ID of the alert.\n     * This field is de/serialized from/to JSON as\n     * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Alert) }`\n     */\n    _id?: MongoId;\n    /** Unix timestamp in milliseconds the alert was opened */\n    ts: I64;\n    /** Whether the alert is already resolved */\n    resolved: boolean;\n    /** The severity of the alert */\n    level: SeverityLevel;\n    /** The target of the alert */\n    target: ResourceTarget;\n    /** The data attached to the alert */\n    data: AlertData;\n    /** The timestamp of alert resolution */\n    resolved_ts?: I64;\n}\nexport type GetAlertResponse = Alert;\nexport type GetAlerterResponse = Alerter;\nexport interface BuildActionState {\n    building: boolean;\n}\nexport type GetBuildActionStateResponse = BuildActionState;\nexport type GetBuildResponse = Build;\nexport type GetBuilderResponse = Builder;\nexport type GetContainerLogResponse = Log;\nexport interface DeploymentActionState {\n    pulling: boolean;\n    deploying: boolean;\n    starting: boolean;\n    restarting: boolean;\n    pausing: boolean;\n    unpausing: boolean;\n    stopping: boolean;\n    destroying: boolean;\n    renaming: boolean;\n}\nexport type GetDeploymentActionStateResponse = DeploymentActionState;\nexport type GetDeploymentLogResponse = Log;\nexport type GetDeploymentResponse = Deployment;\nexport interface ContainerStats {\n    name: string;\n    cpu_perc: string;\n    mem_perc: string;\n    mem_usage: string;\n    net_io: string;\n    block_io: string;\n    pids: string;\n}\nexport type GetDeploymentStatsResponse = ContainerStats;\nexport type GetDockerRegistryAccountResponse = DockerRegistryAccount;\nexport type GetGitProviderAccountResponse = GitProviderAccount;\nexport type GetPermissionResponse = PermissionLevelAndSpecifics;\nexport interface ProcedureActionState {\n    running: boolean;\n}\nexport type GetProcedureActionStateResponse = ProcedureActionState;\nexport type GetProcedureResponse = Procedure;\nexport interface RepoActionState {\n    /** Whether Repo currently cloning on the attached Server */\n    cloning: boolean;\n    /** Whether Repo currently pulling on the attached Server */\n    pulling: boolean;\n    /** Whether Repo currently building using the attached Builder. */\n    building: boolean;\n    /** Whether Repo currently renaming. */\n    renaming: boolean;\n}\nexport type GetRepoActionStateResponse = RepoActionState;\nexport interface RepoConfig {\n    /** The server to clone the repo on. */\n    server_id?: string;\n    /** Attach a builder to 'build' the repo. */\n    builder_id?: string;\n    /** The git provider domain. Default: github.com */\n    git_provider: string;\n    /**\n     * Whether to use https to clone the repo (versus http). Default: true\n     *\n     * Note. Komodo does not currently support cloning repos via ssh.\n     */\n    git_https: boolean;\n    /**\n     * The git account used to access private repos.\n     * Passing empty string can only clone public repos.\n     *\n     * Note. A token for the account must be available in the core config or the builder server's periphery config\n     * for the configured git provider.\n     */\n    git_account?: string;\n    /** The github repo to clone. */\n    repo?: string;\n    /** The repo branch. */\n    branch: string;\n    /** Optionally set a specific commit hash. */\n    commit?: string;\n    /**\n     * Explicitly specify the folder to clone the repo in.\n     * - If absolute (has leading '/')\n     * - Used directly as the path\n     * - If relative\n     * - Taken relative to Periphery `repo_dir` (ie `${root_directory}/repos`)\n     */\n    path?: string;\n    /** Whether incoming webhooks actually trigger action. */\n    webhook_enabled: boolean;\n    /**\n     * Optionally provide an alternate webhook secret for this repo.\n     * If its an empty string, use the default secret from the config.\n     */\n    webhook_secret?: string;\n    /**\n     * Command to be run after the repo is cloned.\n     * The path is relative to the root of the repo.\n     */\n    on_clone?: SystemCommand;\n    /**\n     * Command to be run after the repo is pulled.\n     * The path is relative to the root of the repo.\n     */\n    on_pull?: SystemCommand;\n    /** Configure quick links that are displayed in the resource header */\n    links?: string[];\n    /**\n     * The environment variables passed to the compose file.\n     * They will be written to path defined in env_file_path,\n     * which is given relative to the run directory.\n     *\n     * If it is empty, no file will be written.\n     */\n    environment?: string;\n    /**\n     * The name of the written environment file before `docker compose up`.\n     * Relative to the repo root.\n     * Default: .env\n     */\n    env_file_path: string;\n    /** Whether to skip secret interpolation into the repo environment variable file. */\n    skip_secret_interp?: boolean;\n}\nexport interface RepoInfo {\n    /** When repo was last pulled */\n    last_pulled_at?: I64;\n    /** When repo was last built */\n    last_built_at?: I64;\n    /** Latest built short commit hash, or null. */\n    built_hash?: string;\n    /** Latest built commit message, or null. Only for repo based stacks */\n    built_message?: string;\n    /** Latest remote short commit hash, or null. */\n    latest_hash?: string;\n    /** Latest remote commit message, or null */\n    latest_message?: string;\n}\nexport type Repo = Resource<RepoConfig, RepoInfo>;\nexport type GetRepoResponse = Repo;\nexport interface ResourceSyncActionState {\n    /** Whether sync currently syncing */\n    syncing: boolean;\n}\nexport type GetResourceSyncActionStateResponse = ResourceSyncActionState;\n/** The sync configuration. */\nexport interface ResourceSyncConfig {\n    /** Choose a Komodo Repo (Resource) to source the sync files. */\n    linked_repo?: string;\n    /** The git provider domain. Default: github.com */\n    git_provider: string;\n    /**\n     * Whether to use https to clone the repo (versus http). Default: true\n     *\n     * Note. Komodo does not currently support cloning repos via ssh.\n     */\n    git_https: boolean;\n    /** The Github repo used as the source of the build. */\n    repo?: string;\n    /** The branch of the repo. */\n    branch: string;\n    /** Optionally set a specific commit hash. */\n    commit?: string;\n    /**\n     * The git account used to access private repos.\n     * Passing empty string can only clone public repos.\n     *\n     * Note. A token for the account must be available in the core config or the builder server's periphery config\n     * for the configured git provider.\n     */\n    git_account?: string;\n    /** Whether incoming webhooks actually trigger action. */\n    webhook_enabled: boolean;\n    /**\n     * Optionally provide an alternate webhook secret for this sync.\n     * If its an empty string, use the default secret from the config.\n     */\n    webhook_secret?: string;\n    /**\n     * Files are available on the Komodo Core host.\n     * Specify the file / folder with [ResourceSyncConfig::resource_path].\n     */\n    files_on_host?: boolean;\n    /**\n     * The path of the resource file(s) to sync.\n     * - If Files on Host, this is relative to the configured `sync_directory` in core config.\n     * - If Git Repo based, this is relative to the root of the repo.\n     * Can be a specific file, or a directory containing multiple files / folders.\n     * See [https://komo.do/docs/sync-resources](https://komo.do/docs/sync-resources) for more information.\n     */\n    resource_path?: string[];\n    /**\n     * Enable \"pushes\" to the file,\n     * which exports resources matching tags to single file.\n     * - If using `files_on_host`, it is stored in the file_contents, which must point to a .toml file path (it will be created if it doesn't exist).\n     * - If using `file_contents`, it is stored in the database.\n     * When using this, \"delete\" mode is always enabled.\n     */\n    managed?: boolean;\n    /**\n     * Whether sync should delete resources\n     * not declared in the resource files\n     */\n    delete?: boolean;\n    /**\n     * Whether sync should include resources.\n     * Default: true\n     */\n    include_resources: boolean;\n    /**\n     * When using `managed` resource sync, will only export resources\n     * matching all of the given tags. If none, will match all resources.\n     */\n    match_tags?: string[];\n    /** Whether sync should include variables. */\n    include_variables?: boolean;\n    /** Whether sync should include user groups. */\n    include_user_groups?: boolean;\n    /**\n     * Whether sync should send alert when it enters Pending state.\n     * Default: true\n     */\n    pending_alert: boolean;\n    /** Manage the file contents in the UI. */\n    file_contents?: string;\n}\nexport type DiffData = \n/** Resource will be created */\n{\n    type: \"Create\";\n    data: {\n        /** The name of resource to create */\n        name?: string;\n        /** The proposed resource to create in TOML */\n        proposed: string;\n    };\n} | {\n    type: \"Update\";\n    data: {\n        /** The proposed TOML */\n        proposed: string;\n        /** The current TOML */\n        current: string;\n    };\n} | {\n    type: \"Delete\";\n    data: {\n        /** The current TOML of the resource to delete */\n        current: string;\n    };\n};\nexport interface ResourceDiff {\n    /**\n     * The resource target.\n     * The target id will be empty if \"Create\" ResourceDiffType.\n     */\n    target: ResourceTarget;\n    /** The data associated with the diff. */\n    data: DiffData;\n}\nexport interface SyncDeployUpdate {\n    /** Resources to deploy */\n    to_deploy: number;\n    /** A readable log of all the changes to be applied */\n    log: string;\n}\nexport interface SyncFileContents {\n    /** The base resource path. */\n    resource_path?: string;\n    /** The path of the file / error path relative to the resource path. */\n    path: string;\n    /** The contents of the file */\n    contents: string;\n}\nexport interface ResourceSyncInfo {\n    /** Unix timestamp of last applied sync */\n    last_sync_ts?: I64;\n    /** Short commit hash of last applied sync */\n    last_sync_hash?: string;\n    /** Commit message of last applied sync */\n    last_sync_message?: string;\n    /** The list of pending updates to resources */\n    resource_updates?: ResourceDiff[];\n    /** The list of pending updates to variables */\n    variable_updates?: DiffData[];\n    /** The list of pending updates to user groups */\n    user_group_updates?: DiffData[];\n    /** The list of pending deploys to resources. */\n    pending_deploy?: SyncDeployUpdate;\n    /** If there is an error, it will be stored here */\n    pending_error?: string;\n    /** The commit hash which produced these pending updates. */\n    pending_hash?: string;\n    /** The commit message which produced these pending updates. */\n    pending_message?: string;\n    /** The current sync files */\n    remote_contents?: SyncFileContents[];\n    /** Any read errors in files by path */\n    remote_errors?: SyncFileContents[];\n}\nexport type ResourceSync = Resource<ResourceSyncConfig, ResourceSyncInfo>;\nexport type GetResourceSyncResponse = ResourceSync;\n/** Current pending actions on the server. */\nexport interface ServerActionState {\n    /** Server currently pruning networks */\n    pruning_networks: boolean;\n    /** Server currently pruning containers */\n    pruning_containers: boolean;\n    /** Server currently pruning images */\n    pruning_images: boolean;\n    /** Server currently pruning volumes */\n    pruning_volumes: boolean;\n    /** Server currently pruning docker builders */\n    pruning_builders: boolean;\n    /** Server currently pruning builx cache */\n    pruning_buildx: boolean;\n    /** Server currently pruning system */\n    pruning_system: boolean;\n    /** Server currently starting containers. */\n    starting_containers: boolean;\n    /** Server currently restarting containers. */\n    restarting_containers: boolean;\n    /** Server currently pausing containers. */\n    pausing_containers: boolean;\n    /** Server currently unpausing containers. */\n    unpausing_containers: boolean;\n    /** Server currently stopping containers. */\n    stopping_containers: boolean;\n}\nexport type GetServerActionStateResponse = ServerActionState;\n/** Server configuration. */\nexport interface ServerConfig {\n    /**\n     * The http address of the periphery client.\n     * Default: http://localhost:8120\n     */\n    address: string;\n    /**\n     * The address to use with links for containers on the server.\n     * If empty, will use the 'address' for links.\n     */\n    external_address?: string;\n    /** An optional region label */\n    region?: string;\n    /**\n     * Whether a server is enabled.\n     * If a server is disabled,\n     * you won't be able to perform any actions on it or see deployment's status.\n     * Default: false\n     */\n    enabled: boolean;\n    /**\n     * The timeout used to reach the server in seconds.\n     * default: 2\n     */\n    timeout_seconds: I64;\n    /**\n     * An optional override passkey to use\n     * to authenticate with periphery agent.\n     * If this is empty, will use passkey in core config.\n     */\n    passkey?: string;\n    /**\n     * Sometimes the system stats reports a mount path that is not desired.\n     * Use this field to filter it out from the report.\n     */\n    ignore_mounts?: string[];\n    /**\n     * Whether to monitor any server stats beyond passing health check.\n     * default: true\n     */\n    stats_monitoring: boolean;\n    /**\n     * Whether to trigger 'docker image prune -a -f' every 24 hours.\n     * default: true\n     */\n    auto_prune: boolean;\n    /** Configure quick links that are displayed in the resource header */\n    links?: string[];\n    /** Whether to send alerts about the servers reachability */\n    send_unreachable_alerts: boolean;\n    /** Whether to send alerts about the servers CPU status */\n    send_cpu_alerts: boolean;\n    /** Whether to send alerts about the servers MEM status */\n    send_mem_alerts: boolean;\n    /** Whether to send alerts about the servers DISK status */\n    send_disk_alerts: boolean;\n    /** Whether to send alerts about the servers version mismatch with core */\n    send_version_mismatch_alerts: boolean;\n    /** The percentage threshhold which triggers WARNING state for CPU. */\n    cpu_warning: number;\n    /** The percentage threshhold which triggers CRITICAL state for CPU. */\n    cpu_critical: number;\n    /** The percentage threshhold which triggers WARNING state for MEM. */\n    mem_warning: number;\n    /** The percentage threshhold which triggers CRITICAL state for MEM. */\n    mem_critical: number;\n    /** The percentage threshhold which triggers WARNING state for DISK. */\n    disk_warning: number;\n    /** The percentage threshhold which triggers CRITICAL state for DISK. */\n    disk_critical: number;\n    /** Scheduled maintenance windows during which alerts will be suppressed. */\n    maintenance_windows?: MaintenanceWindow[];\n}\nexport type Server = Resource<ServerConfig, undefined>;\nexport type GetServerResponse = Server;\nexport interface StackActionState {\n    pulling: boolean;\n    deploying: boolean;\n    starting: boolean;\n    restarting: boolean;\n    pausing: boolean;\n    unpausing: boolean;\n    stopping: boolean;\n    destroying: boolean;\n}\nexport type GetStackActionStateResponse = StackActionState;\nexport type GetStackLogResponse = Log;\nexport declare enum StackFileRequires {\n    /** Diff requires service redeploy. */\n    Redeploy = \"Redeploy\",\n    /** Diff requires service restart */\n    Restart = \"Restart\",\n    /** Diff requires no action. Default. */\n    None = \"None\"\n}\n/** Configure additional file dependencies of the Stack. */\nexport interface StackFileDependency {\n    /** Specify the file */\n    path: string;\n    /** Specify specific service/s */\n    services?: string[];\n    /** Specify */\n    requires?: StackFileRequires;\n}\n/** The compose file configuration. */\nexport interface StackConfig {\n    /** The server to deploy the stack on. */\n    server_id?: string;\n    /** Configure quick links that are displayed in the resource header */\n    links?: string[];\n    /**\n     * Optionally specify a custom project name for the stack.\n     * If this is empty string, it will default to the stack name.\n     * Used with `docker compose -p {project_name}`.\n     *\n     * Note. Can be used to import pre-existing stacks.\n     */\n    project_name?: string;\n    /**\n     * Whether to automatically `compose pull` before redeploying stack.\n     * Ensured latest images are deployed.\n     * Will fail if the compose file specifies a locally build image.\n     */\n    auto_pull: boolean;\n    /**\n     * Whether to `docker compose build` before `compose down` / `compose up`.\n     * Combine with build_extra_args for custom behaviors.\n     */\n    run_build?: boolean;\n    /** Whether to poll for any updates to the images. */\n    poll_for_updates?: boolean;\n    /**\n     * Whether to automatically redeploy when\n     * newer images are found. Will implicitly\n     * enable `poll_for_updates`, you don't need to\n     * enable both.\n     */\n    auto_update?: boolean;\n    /**\n     * If auto update is enabled, Komodo will\n     * by default only update the specific services\n     * with image updates. If this parameter is set to true,\n     * Komodo will redeploy the whole Stack (all services).\n     */\n    auto_update_all_services?: boolean;\n    /** Whether to run `docker compose down` before `compose up`. */\n    destroy_before_deploy?: boolean;\n    /** Whether to skip secret interpolation into the stack environment variables. */\n    skip_secret_interp?: boolean;\n    /** Choose a Komodo Repo (Resource) to source the compose files. */\n    linked_repo?: string;\n    /** The git provider domain. Default: github.com */\n    git_provider: string;\n    /**\n     * Whether to use https to clone the repo (versus http). Default: true\n     *\n     * Note. Komodo does not currently support cloning repos via ssh.\n     */\n    git_https: boolean;\n    /**\n     * The git account used to access private repos.\n     * Passing empty string can only clone public repos.\n     *\n     * Note. A token for the account must be available in the core config or the builder server's periphery config\n     * for the configured git provider.\n     */\n    git_account?: string;\n    /**\n     * The repo used as the source of the build.\n     * {namespace}/{repo_name}\n     */\n    repo?: string;\n    /** The branch of the repo. */\n    branch: string;\n    /** Optionally set a specific commit hash. */\n    commit?: string;\n    /** Optionally set a specific clone path */\n    clone_path?: string;\n    /**\n     * By default, the Stack will `git pull` the repo after it is first cloned.\n     * If this option is enabled, the repo folder will be deleted and recloned instead.\n     */\n    reclone?: boolean;\n    /** Whether incoming webhooks actually trigger action. */\n    webhook_enabled: boolean;\n    /**\n     * Optionally provide an alternate webhook secret for this stack.\n     * If its an empty string, use the default secret from the config.\n     */\n    webhook_secret?: string;\n    /**\n     * By default, the Stack will `DeployStackIfChanged`.\n     * If this option is enabled, will always run `DeployStack` without diffing.\n     */\n    webhook_force_deploy?: boolean;\n    /**\n     * If this is checked, the stack will source the files on the host.\n     * Use `run_directory` and `file_paths` to specify the path on the host.\n     * This is useful for those who wish to setup their files on the host,\n     * rather than defining the contents in UI or in a git repo.\n     */\n    files_on_host?: boolean;\n    /** Directory to change to (`cd`) before running `docker compose up -d`. */\n    run_directory?: string;\n    /**\n     * Add paths to compose files, relative to the run path.\n     * If this is empty, will use file `compose.yaml`.\n     */\n    file_paths?: string[];\n    /**\n     * The name of the written environment file before `docker compose up`.\n     * Relative to the run directory root.\n     * Default: .env\n     */\n    env_file_path: string;\n    /**\n     * Add additional env files to attach with `--env-file`.\n     * Relative to the run directory root.\n     *\n     * Note. It is already included as an `additional_file`.\n     * Don't add it again there.\n     */\n    additional_env_files?: string[];\n    /**\n     * Add additional config files either in repo or on host to track.\n     * Can add any files associated with the stack to enable editing them in the UI.\n     * Doing so will also include diffing these when deciding to deploy in `DeployStackIfChanged`.\n     * Relative to the run directory.\n     *\n     * Note. If the config file is .env and should be included in compose command\n     * using `--env-file`, add it to `additional_env_files` instead.\n     */\n    config_files?: StackFileDependency[];\n    /** Whether to send StackStateChange alerts for this stack. */\n    send_alerts: boolean;\n    /** Used with `registry_account` to login to a registry before docker compose up. */\n    registry_provider?: string;\n    /** Used with `registry_provider` to login to a registry before docker compose up. */\n    registry_account?: string;\n    /** The optional command to run before the Stack is deployed. */\n    pre_deploy?: SystemCommand;\n    /** The optional command to run after the Stack is deployed. */\n    post_deploy?: SystemCommand;\n    /**\n     * The extra arguments to pass after `docker compose up -d`.\n     * If empty, no extra arguments will be passed.\n     */\n    extra_args?: string[];\n    /**\n     * The extra arguments to pass after `docker compose build`.\n     * If empty, no extra build arguments will be passed.\n     * Only used if `run_build: true`\n     */\n    build_extra_args?: string[];\n    /**\n     * Ignore certain services declared in the compose file when checking\n     * the stack status. For example, an init service might be exited, but the\n     * stack should be healthy. This init service should be in `ignore_services`\n     */\n    ignore_services?: string[];\n    /**\n     * The contents of the file directly, for management in the UI.\n     * If this is empty, it will fall back to checking git config for\n     * repo based compose file.\n     * Supports variable / secret interpolation.\n     */\n    file_contents?: string;\n    /**\n     * The environment variables passed to the compose file.\n     * They will be written to path defined in env_file_path,\n     * which is given relative to the run directory.\n     *\n     * If it is empty, no file will be written.\n     */\n    environment?: string;\n}\nexport interface FileContents {\n    /** The path to the file */\n    path: string;\n    /** The contents of the file */\n    contents: string;\n}\nexport interface StackServiceNames {\n    /** The name of the service */\n    service_name: string;\n    /**\n     * Will either be the declared container_name in the compose file,\n     * or a pattern to match auto named containers.\n     *\n     * Auto named containers are composed of three parts:\n     *\n     * 1. The name of the compose project (top level name field of compose file).\n     * This defaults to the name of the parent folder of the compose file.\n     * Komodo will always set it to be the name of the stack, but imported stacks\n     * will have a different name.\n     * 2. The service name\n     * 3. The replica number\n     *\n     * Example: stacko-mongo-1.\n     *\n     * This stores only 1. and 2., ie stacko-mongo.\n     * Containers will be matched via regex like `^container_name-?[0-9]*$``\n     */\n    container_name: string;\n    /** The services image. */\n    image?: string;\n}\n/**\n * Same as [FileContents] with some extra\n * info specific to Stacks.\n */\nexport interface StackRemoteFileContents {\n    /** The path to the file */\n    path: string;\n    /** The contents of the file */\n    contents: string;\n    /**\n     * The services depending on this file,\n     * or empty for global requirement (eg all compose files and env files).\n     */\n    services?: string[];\n    /** Whether diff requires Redeploy / Restart / None */\n    requires?: StackFileRequires;\n}\nexport interface StackInfo {\n    /**\n     * If any of the expected compose / additional files are missing in the repo,\n     * they will be stored here.\n     */\n    missing_files?: string[];\n    /**\n     * The deployed project name.\n     * This is updated whenever Komodo successfully deploys the stack.\n     * If it is present, Komodo will use it for actions over other options,\n     * to ensure control is maintained after changing the project name (there is no rename compose project api).\n     */\n    deployed_project_name?: string;\n    /** Deployed short commit hash, or null. Only for repo based stacks. */\n    deployed_hash?: string;\n    /** Deployed commit message, or null. Only for repo based stacks */\n    deployed_message?: string;\n    /**\n     * The deployed compose / additional file contents.\n     * This is updated whenever Komodo successfully deploys the stack.\n     */\n    deployed_contents?: FileContents[];\n    /**\n     * The deployed service names.\n     * This is updated whenever it is empty, or deployed contents is updated.\n     */\n    deployed_services?: StackServiceNames[];\n    /**\n     * The output of `docker compose config`.\n     * This is updated whenever Komodo successfully deploys the stack.\n     */\n    deployed_config?: string;\n    /**\n     * The latest service names.\n     * This is updated whenever the stack cache refreshes, using the latest file contents (either db defined or remote).\n     */\n    latest_services?: StackServiceNames[];\n    /**\n     * The remote compose / additional file contents, whether on host or in repo.\n     * This is updated whenever Komodo refreshes the stack cache.\n     * It will be empty if the file is defined directly in the stack config.\n     */\n    remote_contents?: StackRemoteFileContents[];\n    /** If there was an error in getting the remote contents, it will be here. */\n    remote_errors?: FileContents[];\n    /** Latest commit hash, or null */\n    latest_hash?: string;\n    /** Latest commit message, or null */\n    latest_message?: string;\n}\nexport type Stack = Resource<StackConfig, StackInfo>;\nexport type GetStackResponse = Stack;\n/** System information of a server */\nexport interface SystemInformation {\n    /** The system name */\n    name?: string;\n    /** The system long os version */\n    os?: string;\n    /** System's kernel version */\n    kernel?: string;\n    /** Physical core count */\n    core_count?: number;\n    /** System hostname based off DNS */\n    host_name?: string;\n    /** The CPU's brand */\n    cpu_brand: string;\n    /** Whether terminals are disabled on this Periphery server */\n    terminals_disabled: boolean;\n    /** Whether container exec is disabled on this Periphery server */\n    container_exec_disabled: boolean;\n}\nexport type GetSystemInformationResponse = SystemInformation;\nexport interface SystemLoadAverage {\n    /** 1m load average */\n    one: number;\n    /** 5m load average */\n    five: number;\n    /** 15m load average */\n    fifteen: number;\n}\n/** Info for a single disk mounted on the system. */\nexport interface SingleDiskUsage {\n    /** The mount point of the disk */\n    mount: string;\n    /** Detected file system */\n    file_system: string;\n    /** Used portion of the disk in GB */\n    used_gb: number;\n    /** Total size of the disk in GB */\n    total_gb: number;\n}\nexport declare enum Timelength {\n    /** `1-sec` */\n    OneSecond = \"1-sec\",\n    /** `5-sec` */\n    FiveSeconds = \"5-sec\",\n    /** `10-sec` */\n    TenSeconds = \"10-sec\",\n    /** `15-sec` */\n    FifteenSeconds = \"15-sec\",\n    /** `30-sec` */\n    ThirtySeconds = \"30-sec\",\n    /** `1-min` */\n    OneMinute = \"1-min\",\n    /** `2-min` */\n    TwoMinutes = \"2-min\",\n    /** `5-min` */\n    FiveMinutes = \"5-min\",\n    /** `10-min` */\n    TenMinutes = \"10-min\",\n    /** `15-min` */\n    FifteenMinutes = \"15-min\",\n    /** `30-min` */\n    ThirtyMinutes = \"30-min\",\n    /** `1-hr` */\n    OneHour = \"1-hr\",\n    /** `2-hr` */\n    TwoHours = \"2-hr\",\n    /** `6-hr` */\n    SixHours = \"6-hr\",\n    /** `8-hr` */\n    EightHours = \"8-hr\",\n    /** `12-hr` */\n    TwelveHours = \"12-hr\",\n    /** `1-day` */\n    OneDay = \"1-day\",\n    /** `3-day` */\n    ThreeDay = \"3-day\",\n    /** `1-wk` */\n    OneWeek = \"1-wk\",\n    /** `2-wk` */\n    TwoWeeks = \"2-wk\",\n    /** `30-day` */\n    ThirtyDays = \"30-day\"\n}\n/** Realtime system stats data. */\nexport interface SystemStats {\n    /** Cpu usage percentage */\n    cpu_perc: number;\n    /** Load average (1m, 5m, 15m) */\n    load_average?: SystemLoadAverage;\n    /**\n     * [1.15.9+]\n     * Free memory in GB.\n     * This is really the 'Free' memory, not the 'Available' memory.\n     * It may be different than mem_total_gb - mem_used_gb.\n     */\n    mem_free_gb?: number;\n    /** Used memory in GB. 'Total' - 'Available' (not free) memory. */\n    mem_used_gb: number;\n    /** Total memory in GB */\n    mem_total_gb: number;\n    /** Breakdown of individual disks, ie their usages, sizes, and mount points */\n    disks: SingleDiskUsage[];\n    /** Network ingress usage in MB */\n    network_ingress_bytes?: number;\n    /** Network egress usage in MB */\n    network_egress_bytes?: number;\n    /** The rate the system stats are being polled from the system */\n    polling_rate: Timelength;\n    /** Unix timestamp in milliseconds when stats were last polled */\n    refresh_ts: I64;\n    /** Unix timestamp in milliseconds when disk list was last refreshed */\n    refresh_list_ts: I64;\n}\nexport type GetSystemStatsResponse = SystemStats;\nexport declare enum TagColor {\n    LightSlate = \"LightSlate\",\n    Slate = \"Slate\",\n    DarkSlate = \"DarkSlate\",\n    LightRed = \"LightRed\",\n    Red = \"Red\",\n    DarkRed = \"DarkRed\",\n    LightOrange = \"LightOrange\",\n    Orange = \"Orange\",\n    DarkOrange = \"DarkOrange\",\n    LightAmber = \"LightAmber\",\n    Amber = \"Amber\",\n    DarkAmber = \"DarkAmber\",\n    LightYellow = \"LightYellow\",\n    Yellow = \"Yellow\",\n    DarkYellow = \"DarkYellow\",\n    LightLime = \"LightLime\",\n    Lime = \"Lime\",\n    DarkLime = \"DarkLime\",\n    LightGreen = \"LightGreen\",\n    Green = \"Green\",\n    DarkGreen = \"DarkGreen\",\n    LightEmerald = \"LightEmerald\",\n    Emerald = \"Emerald\",\n    DarkEmerald = \"DarkEmerald\",\n    LightTeal = \"LightTeal\",\n    Teal = \"Teal\",\n    DarkTeal = \"DarkTeal\",\n    LightCyan = \"LightCyan\",\n    Cyan = \"Cyan\",\n    DarkCyan = \"DarkCyan\",\n    LightSky = \"LightSky\",\n    Sky = \"Sky\",\n    DarkSky = \"DarkSky\",\n    LightBlue = \"LightBlue\",\n    Blue = \"Blue\",\n    DarkBlue = \"DarkBlue\",\n    LightIndigo = \"LightIndigo\",\n    Indigo = \"Indigo\",\n    DarkIndigo = \"DarkIndigo\",\n    LightViolet = \"LightViolet\",\n    Violet = \"Violet\",\n    DarkViolet = \"DarkViolet\",\n    LightPurple = \"LightPurple\",\n    Purple = \"Purple\",\n    DarkPurple = \"DarkPurple\",\n    LightFuchsia = \"LightFuchsia\",\n    Fuchsia = \"Fuchsia\",\n    DarkFuchsia = \"DarkFuchsia\",\n    LightPink = \"LightPink\",\n    Pink = \"Pink\",\n    DarkPink = \"DarkPink\",\n    LightRose = \"LightRose\",\n    Rose = \"Rose\",\n    DarkRose = \"DarkRose\"\n}\nexport interface Tag {\n    /**\n     * The Mongo ID of the tag.\n     * This field is de/serialized from/to JSON as\n     * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized Tag) }`\n     */\n    _id?: MongoId;\n    name: string;\n    owner?: string;\n    /** Hex color code with alpha for UI display */\n    color?: TagColor;\n}\nexport type GetTagResponse = Tag;\nexport type GetUpdateResponse = Update;\n/**\n * Permission users at the group level.\n *\n * All users that are part of a group inherit the group's permissions.\n * A user can be a part of multiple groups. A user's permission on a particular resource\n * will be resolved to be the maximum permission level between the user's own permissions and\n * any groups they are a part of.\n */\nexport interface UserGroup {\n    /**\n     * The Mongo ID of the UserGroup.\n     * This field is de/serialized from/to JSON as\n     * `{ \"_id\": { \"$oid\": \"...\" }, ...(rest of serialized User) }`\n     */\n    _id?: MongoId;\n    /** A name for the user group */\n    name: string;\n    /** Whether all users will implicitly have the permissions in this group. */\n    everyone?: boolean;\n    /** User ids of group members */\n    users?: string[];\n    /** Give the user group elevated permissions on all resources of a certain type */\n    all?: Record<ResourceTarget[\"type\"], PermissionLevelAndSpecifics | PermissionLevel>;\n    /** Unix time (ms) when user group last updated */\n    updated_at?: I64;\n}\nexport type GetUserGroupResponse = UserGroup;\nexport type GetUserResponse = User;\nexport type GetVariableResponse = Variable;\nexport declare enum ContainerStateStatusEnum {\n    Running = \"running\",\n    Created = \"created\",\n    Paused = \"paused\",\n    Restarting = \"restarting\",\n    Exited = \"exited\",\n    Removing = \"removing\",\n    Dead = \"dead\",\n    Empty = \"\"\n}\nexport declare enum HealthStatusEnum {\n    Empty = \"\",\n    None = \"none\",\n    Starting = \"starting\",\n    Healthy = \"healthy\",\n    Unhealthy = \"unhealthy\"\n}\n/** HealthcheckResult stores information about a single run of a healthcheck probe */\nexport interface HealthcheckResult {\n    /** Date and time at which this check started in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. */\n    Start?: string;\n    /** Date and time at which this check ended in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. */\n    End?: string;\n    /** ExitCode meanings:  - `0` healthy - `1` unhealthy - `2` reserved (considered unhealthy) - other values: error running probe */\n    ExitCode?: I64;\n    /** Output from last check */\n    Output?: string;\n}\n/** Health stores information about the container's healthcheck results. */\nexport interface ContainerHealth {\n    /** Status is one of `none`, `starting`, `healthy` or `unhealthy`  - \\\"none\\\"      Indicates there is no healthcheck - \\\"starting\\\"  Starting indicates that the container is not yet ready - \\\"healthy\\\"   Healthy indicates that the container is running correctly - \\\"unhealthy\\\" Unhealthy indicates that the container has a problem */\n    Status?: HealthStatusEnum;\n    /** FailingStreak is the number of consecutive failures */\n    FailingStreak?: I64;\n    /** Log contains the last few results (oldest first) */\n    Log?: HealthcheckResult[];\n}\n/** ContainerState stores container's running state. It's part of ContainerJSONBase and will be returned by the \\\"inspect\\\" command. */\nexport interface ContainerState {\n    /** String representation of the container state. Can be one of \\\"created\\\", \\\"running\\\", \\\"paused\\\", \\\"restarting\\\", \\\"removing\\\", \\\"exited\\\", or \\\"dead\\\". */\n    Status?: ContainerStateStatusEnum;\n    /** Whether this container is running.  Note that a running container can be _paused_. The `Running` and `Paused` booleans are not mutually exclusive:  When pausing a container (on Linux), the freezer cgroup is used to suspend all processes in the container. Freezing the process requires the process to be running. As a result, paused containers are both `Running` _and_ `Paused`.  Use the `Status` field instead to determine if a container's state is \\\"running\\\". */\n    Running?: boolean;\n    /** Whether this container is paused. */\n    Paused?: boolean;\n    /** Whether this container is restarting. */\n    Restarting?: boolean;\n    /** Whether a process within this container has been killed because it ran out of memory since the container was last started. */\n    OOMKilled?: boolean;\n    Dead?: boolean;\n    /** The process ID of this container */\n    Pid?: I64;\n    /** The last exit code of this container */\n    ExitCode?: I64;\n    Error?: string;\n    /** The time when this container was last started. */\n    StartedAt?: string;\n    /** The time when this container last exited. */\n    FinishedAt?: string;\n    Health?: ContainerHealth;\n}\nexport type Usize = number;\nexport interface ResourcesBlkioWeightDevice {\n    Path?: string;\n    Weight?: Usize;\n}\nexport interface ThrottleDevice {\n    /** Device path */\n    Path?: string;\n    /** Rate */\n    Rate?: I64;\n}\n/** A device mapping between the host and container */\nexport interface DeviceMapping {\n    PathOnHost?: string;\n    PathInContainer?: string;\n    CgroupPermissions?: string;\n}\n/** A request for devices to be sent to device drivers */\nexport interface DeviceRequest {\n    Driver?: string;\n    Count?: I64;\n    DeviceIDs?: string[];\n    /** A list of capabilities; an OR list of AND lists of capabilities. */\n    Capabilities?: string[][];\n    /** Driver-specific options, specified as a key/value pairs. These options are passed directly to the driver. */\n    Options?: Record<string, string>;\n}\nexport interface ResourcesUlimits {\n    /** Name of ulimit */\n    Name?: string;\n    /** Soft limit */\n    Soft?: I64;\n    /** Hard limit */\n    Hard?: I64;\n}\n/** The logging configuration for this container */\nexport interface HostConfigLogConfig {\n    Type?: string;\n    Config?: Record<string, string>;\n}\n/** PortBinding represents a binding between a host IP address and a host port. */\nexport interface PortBinding {\n    /** Host IP address that the container's port is mapped to. */\n    HostIp?: string;\n    /** Host port number that the container's port is mapped to. */\n    HostPort?: string;\n}\nexport declare enum RestartPolicyNameEnum {\n    Empty = \"\",\n    No = \"no\",\n    Always = \"always\",\n    UnlessStopped = \"unless-stopped\",\n    OnFailure = \"on-failure\"\n}\n/** The behavior to apply when the container exits. The default is not to restart.  An ever increasing delay (double the previous delay, starting at 100ms) is added before each restart to prevent flooding the server. */\nexport interface RestartPolicy {\n    /** - Empty string means not to restart - `no` Do not automatically restart - `always` Always restart - `unless-stopped` Restart always except when the user has manually stopped the container - `on-failure` Restart only when the container exit code is non-zero */\n    Name?: RestartPolicyNameEnum;\n    /** If `on-failure` is used, the number of times to retry before giving up. */\n    MaximumRetryCount?: I64;\n}\nexport declare enum MountTypeEnum {\n    Empty = \"\",\n    Bind = \"bind\",\n    Volume = \"volume\",\n    Image = \"image\",\n    Tmpfs = \"tmpfs\",\n    Npipe = \"npipe\",\n    Cluster = \"cluster\"\n}\nexport declare enum MountBindOptionsPropagationEnum {\n    Empty = \"\",\n    Private = \"private\",\n    Rprivate = \"rprivate\",\n    Shared = \"shared\",\n    Rshared = \"rshared\",\n    Slave = \"slave\",\n    Rslave = \"rslave\"\n}\n/** Optional configuration for the `bind` type. */\nexport interface MountBindOptions {\n    /** A propagation mode with the value `[r]private`, `[r]shared`, or `[r]slave`. */\n    Propagation?: MountBindOptionsPropagationEnum;\n    /** Disable recursive bind mount. */\n    NonRecursive?: boolean;\n    /** Create mount point on host if missing */\n    CreateMountpoint?: boolean;\n    /** Make the mount non-recursively read-only, but still leave the mount recursive (unless NonRecursive is set to `true` in conjunction).  Addded in v1.44, before that version all read-only mounts were non-recursive by default. To match the previous behaviour this will default to `true` for clients on versions prior to v1.44. */\n    ReadOnlyNonRecursive?: boolean;\n    /** Raise an error if the mount cannot be made recursively read-only. */\n    ReadOnlyForceRecursive?: boolean;\n}\n/** Map of driver specific options */\nexport interface MountVolumeOptionsDriverConfig {\n    /** Name of the driver to use to create the volume. */\n    Name?: string;\n    /** key/value map of driver specific options. */\n    Options?: Record<string, string>;\n}\n/** Optional configuration for the `volume` type. */\nexport interface MountVolumeOptions {\n    /** Populate volume with data from the target. */\n    NoCopy?: boolean;\n    /** User-defined key/value metadata. */\n    Labels?: Record<string, string>;\n    DriverConfig?: MountVolumeOptionsDriverConfig;\n    /** Source path inside the volume. Must be relative without any back traversals. */\n    Subpath?: string;\n}\n/** Optional configuration for the `tmpfs` type. */\nexport interface MountTmpfsOptions {\n    /** The size for the tmpfs mount in bytes. */\n    SizeBytes?: I64;\n    /** The permission mode for the tmpfs mount in an integer. */\n    Mode?: I64;\n}\nexport interface ContainerMount {\n    /** Container path. */\n    Target?: string;\n    /** Mount source (e.g. a volume name, a host path). */\n    Source?: string;\n    /** The mount type. Available types:  - `bind` Mounts a file or directory from the host into the container. Must exist prior to creating the container. - `volume` Creates a volume with the given name and options (or uses a pre-existing volume with the same name and options). These are **not** removed when the container is removed. - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container. - `cluster` a Swarm cluster volume */\n    Type?: MountTypeEnum;\n    /** Whether the mount should be read-only. */\n    ReadOnly?: boolean;\n    /** The consistency requirement for the mount: `default`, `consistent`, `cached`, or `delegated`. */\n    Consistency?: string;\n    BindOptions?: MountBindOptions;\n    VolumeOptions?: MountVolumeOptions;\n    TmpfsOptions?: MountTmpfsOptions;\n}\nexport declare enum HostConfigCgroupnsModeEnum {\n    Empty = \"\",\n    Private = \"private\",\n    Host = \"host\"\n}\nexport declare enum HostConfigIsolationEnum {\n    Empty = \"\",\n    Default = \"default\",\n    Process = \"process\",\n    Hyperv = \"hyperv\"\n}\n/** Container configuration that depends on the host we are running on */\nexport interface HostConfig {\n    /** An integer value representing this container's relative CPU weight versus other containers. */\n    CpuShares?: I64;\n    /** Memory limit in bytes. */\n    Memory?: I64;\n    /** Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist. */\n    CgroupParent?: string;\n    /** Block IO weight (relative weight). */\n    BlkioWeight?: number;\n    /** Block IO weight (relative device weight) in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Weight\\\": weight}] ``` */\n    BlkioWeightDevice?: ResourcesBlkioWeightDevice[];\n    /** Limit read rate (bytes per second) from a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ``` */\n    BlkioDeviceReadBps?: ThrottleDevice[];\n    /** Limit write rate (bytes per second) to a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ``` */\n    BlkioDeviceWriteBps?: ThrottleDevice[];\n    /** Limit read rate (IO per second) from a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ``` */\n    BlkioDeviceReadIOps?: ThrottleDevice[];\n    /** Limit write rate (IO per second) to a device, in the form:  ``` [{\\\"Path\\\": \\\"device_path\\\", \\\"Rate\\\": rate}] ``` */\n    BlkioDeviceWriteIOps?: ThrottleDevice[];\n    /** The length of a CPU period in microseconds. */\n    CpuPeriod?: I64;\n    /** Microseconds of CPU time that the container can get in a CPU period. */\n    CpuQuota?: I64;\n    /** The length of a CPU real-time period in microseconds. Set to 0 to allocate no time allocated to real-time tasks. */\n    CpuRealtimePeriod?: I64;\n    /** The length of a CPU real-time runtime in microseconds. Set to 0 to allocate no time allocated to real-time tasks. */\n    CpuRealtimeRuntime?: I64;\n    /** CPUs in which to allow execution (e.g., `0-3`, `0,1`). */\n    CpusetCpus?: string;\n    /** Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. */\n    CpusetMems?: string;\n    /** A list of devices to add to the container. */\n    Devices?: DeviceMapping[];\n    /** a list of cgroup rules to apply to the container */\n    DeviceCgroupRules?: string[];\n    /** A list of requests for devices to be sent to device drivers. */\n    DeviceRequests?: DeviceRequest[];\n    /** Hard limit for kernel TCP buffer memory (in bytes). Depending on the OCI runtime in use, this option may be ignored. It is no longer supported by the default (runc) runtime.  This field is omitted when empty. */\n    KernelMemoryTCP?: I64;\n    /** Memory soft limit in bytes. */\n    MemoryReservation?: I64;\n    /** Total memory limit (memory + swap). Set as `-1` to enable unlimited swap. */\n    MemorySwap?: I64;\n    /** Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. */\n    MemorySwappiness?: I64;\n    /** CPU quota in units of 10<sup>-9</sup> CPUs. */\n    NanoCpus?: I64;\n    /** Disable OOM Killer for the container. */\n    OomKillDisable?: boolean;\n    /** Run an init inside the container that forwards signals and reaps processes. This field is omitted if empty, and the default (as configured on the daemon) is used. */\n    Init?: boolean;\n    /** Tune a container's PIDs limit. Set `0` or `-1` for unlimited, or `null` to not change. */\n    PidsLimit?: I64;\n    /** A list of resource limits to set in the container. For example:  ``` {\\\"Name\\\": \\\"nofile\\\", \\\"Soft\\\": 1024, \\\"Hard\\\": 2048} ``` */\n    Ulimits?: ResourcesUlimits[];\n    /** The number of usable CPUs (Windows only).  On Windows Server containers, the processor resource controls are mutually exclusive. The order of precedence is `CPUCount` first, then `CPUShares`, and `CPUPercent` last. */\n    CpuCount?: I64;\n    /** The usable percentage of the available CPUs (Windows only).  On Windows Server containers, the processor resource controls are mutually exclusive. The order of precedence is `CPUCount` first, then `CPUShares`, and `CPUPercent` last. */\n    CpuPercent?: I64;\n    /** Maximum IOps for the container system drive (Windows only) */\n    IOMaximumIOps?: I64;\n    /** Maximum IO in bytes per second for the container system drive (Windows only). */\n    IOMaximumBandwidth?: I64;\n    /** A list of volume bindings for this container. Each volume binding is a string in one of these forms:  - `host-src:container-dest[:options]` to bind-mount a host path   into the container. Both `host-src`, and `container-dest` must   be an _absolute_ path. - `volume-name:container-dest[:options]` to bind-mount a volume   managed by a volume driver into the container. `container-dest`   must be an _absolute_ path.  `options` is an optional, comma-delimited list of:  - `nocopy` disables automatic copying of data from the container   path to the volume. The `nocopy` flag only applies to named volumes. - `[ro|rw]` mounts a volume read-only or read-write, respectively.   If omitted or set to `rw`, volumes are mounted read-write. - `[z|Z]` applies SELinux labels to allow or deny multiple containers   to read and write to the same volume.     - `z`: a _shared_ content label is applied to the content. This       label indicates that multiple containers can share the volume       content, for both reading and writing.     - `Z`: a _private unshared_ label is applied to the content.       This label indicates that only the current container can use       a private volume. Labeling systems such as SELinux require       proper labels to be placed on volume content that is mounted       into a container. Without a label, the security system can       prevent a container's processes from using the content. By       default, the labels set by the host operating system are not       modified. - `[[r]shared|[r]slave|[r]private]` specifies mount   [propagation behavior](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt).   This only applies to bind-mounted volumes, not internal volumes   or named volumes. Mount propagation requires the source mount   point (the location where the source directory is mounted in the   host operating system) to have the correct propagation properties.   For shared volumes, the source mount point must be set to `shared`.   For slave volumes, the mount must be set to either `shared` or   `slave`. */\n    Binds?: string[];\n    /** Path to a file where the container ID is written */\n    ContainerIDFile?: string;\n    LogConfig?: HostConfigLogConfig;\n    /** Network mode to use for this container. Supported standard values are: `bridge`, `host`, `none`, and `container:<name|id>`. Any other value is taken as a custom network's name to which this container should connect to. */\n    NetworkMode?: string;\n    PortBindings?: Record<string, PortBinding[]>;\n    RestartPolicy?: RestartPolicy;\n    /** Automatically remove the container when the container's process exits. This has no effect if `RestartPolicy` is set. */\n    AutoRemove?: boolean;\n    /** Driver that this container uses to mount volumes. */\n    VolumeDriver?: string;\n    /** A list of volumes to inherit from another container, specified in the form `<container name>[:<ro|rw>]`. */\n    VolumesFrom?: string[];\n    /** Specification for mounts to be added to the container. */\n    Mounts?: ContainerMount[];\n    /** Initial console size, as an `[height, width]` array. */\n    ConsoleSize?: number[];\n    /** Arbitrary non-identifying metadata attached to container and provided to the runtime when the container is started. */\n    Annotations?: Record<string, string>;\n    /** A list of kernel capabilities to add to the container. Conflicts with option 'Capabilities'. */\n    CapAdd?: string[];\n    /** A list of kernel capabilities to drop from the container. Conflicts with option 'Capabilities'. */\n    CapDrop?: string[];\n    /** cgroup namespace mode for the container. Possible values are:  - `\\\"private\\\"`: the container runs in its own private cgroup namespace - `\\\"host\\\"`: use the host system's cgroup namespace  If not specified, the daemon default is used, which can either be `\\\"private\\\"` or `\\\"host\\\"`, depending on daemon version, kernel support and configuration. */\n    CgroupnsMode?: HostConfigCgroupnsModeEnum;\n    /** A list of DNS servers for the container to use. */\n    Dns?: string[];\n    /** A list of DNS options. */\n    DnsOptions?: string[];\n    /** A list of DNS search domains. */\n    DnsSearch?: string[];\n    /** A list of hostnames/IP mappings to add to the container's `/etc/hosts` file. Specified in the form `[\\\"hostname:IP\\\"]`. */\n    ExtraHosts?: string[];\n    /** A list of additional groups that the container process will run as. */\n    GroupAdd?: string[];\n    /** IPC sharing mode for the container. Possible values are:  - `\\\"none\\\"`: own private IPC namespace, with /dev/shm not mounted - `\\\"private\\\"`: own private IPC namespace - `\\\"shareable\\\"`: own private IPC namespace, with a possibility to share it with other containers - `\\\"container:<name|id>\\\"`: join another (shareable) container's IPC namespace - `\\\"host\\\"`: use the host system's IPC namespace  If not specified, daemon default is used, which can either be `\\\"private\\\"` or `\\\"shareable\\\"`, depending on daemon version and configuration. */\n    IpcMode?: string;\n    /** Cgroup to use for the container. */\n    Cgroup?: string;\n    /** A list of links for the container in the form `container_name:alias`. */\n    Links?: string[];\n    /** An integer value containing the score given to the container in order to tune OOM killer preferences. */\n    OomScoreAdj?: I64;\n    /** Set the PID (Process) Namespace mode for the container. It can be either:  - `\\\"container:<name|id>\\\"`: joins another container's PID namespace - `\\\"host\\\"`: use the host's PID namespace inside the container */\n    PidMode?: string;\n    /** Gives the container full access to the host. */\n    Privileged?: boolean;\n    /** Allocates an ephemeral host port for all of a container's exposed ports.  Ports are de-allocated when the container stops and allocated when the container starts. The allocated port might be changed when restarting the container.  The port is selected from the ephemeral port range that depends on the kernel. For example, on Linux the range is defined by `/proc/sys/net/ipv4/ip_local_port_range`. */\n    PublishAllPorts?: boolean;\n    /** Mount the container's root filesystem as read only. */\n    ReadonlyRootfs?: boolean;\n    /** A list of string values to customize labels for MLS systems, such as SELinux. */\n    SecurityOpt?: string[];\n    /** Storage driver options for this container, in the form `{\\\"size\\\": \\\"120G\\\"}`. */\n    StorageOpt?: Record<string, string>;\n    /** A map of container directories which should be replaced by tmpfs mounts, and their corresponding mount options. For example:  ``` { \\\"/run\\\": \\\"rw,noexec,nosuid,size=65536k\\\" } ``` */\n    Tmpfs?: Record<string, string>;\n    /** UTS namespace to use for the container. */\n    UTSMode?: string;\n    /** Sets the usernamespace mode for the container when usernamespace remapping option is enabled. */\n    UsernsMode?: string;\n    /** Size of `/dev/shm` in bytes. If omitted, the system uses 64MB. */\n    ShmSize?: I64;\n    /** A list of kernel parameters (sysctls) to set in the container. For example:  ``` {\\\"net.ipv4.ip_forward\\\": \\\"1\\\"} ``` */\n    Sysctls?: Record<string, string>;\n    /** Runtime to use with this container. */\n    Runtime?: string;\n    /** Isolation technology of the container. (Windows only) */\n    Isolation?: HostConfigIsolationEnum;\n    /** The list of paths to be masked inside the container (this overrides the default set of paths). */\n    MaskedPaths?: string[];\n    /** The list of paths to be set as read-only inside the container (this overrides the default set of paths). */\n    ReadonlyPaths?: string[];\n}\n/** Information about the storage driver used to store the container's and image's filesystem. */\nexport interface GraphDriverData {\n    /** Name of the storage driver. */\n    Name?: string;\n    /** Low-level storage metadata, provided as key/value pairs.  This information is driver-specific, and depends on the storage-driver in use, and should be used for informational purposes only. */\n    Data?: Record<string, string>;\n}\n/** MountPoint represents a mount point configuration inside the container. This is used for reporting the mountpoints in use by a container. */\nexport interface MountPoint {\n    /** The mount type:  - `bind` a mount of a file or directory from the host into the container. - `volume` a docker volume with the given `Name`. - `tmpfs` a `tmpfs`. - `npipe` a named pipe from the host into the container. - `cluster` a Swarm cluster volume */\n    Type?: MountTypeEnum;\n    /** Name is the name reference to the underlying data defined by `Source` e.g., the volume name. */\n    Name?: string;\n    /** Source location of the mount.  For volumes, this contains the storage location of the volume (within `/var/lib/docker/volumes/`). For bind-mounts, and `npipe`, this contains the source (host) part of the bind-mount. For `tmpfs` mount points, this field is empty. */\n    Source?: string;\n    /** Destination is the path relative to the container root (`/`) where the `Source` is mounted inside the container. */\n    Destination?: string;\n    /** Driver is the volume driver used to create the volume (if it is a volume). */\n    Driver?: string;\n    /** Mode is a comma separated list of options supplied by the user when creating the bind/volume mount.  The default is platform-specific (`\\\"z\\\"` on Linux, empty on Windows). */\n    Mode?: string;\n    /** Whether the mount is mounted writable (read-write). */\n    RW?: boolean;\n    /** Propagation describes how mounts are propagated from the host into the mount point, and vice-versa. Refer to the [Linux kernel documentation](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt) for details. This field is not used on Windows. */\n    Propagation?: string;\n}\n/** A test to perform to check that the container is healthy. */\nexport interface HealthConfig {\n    /** The test to perform. Possible values are:  - `[]` inherit healthcheck from image or parent image - `[\\\"NONE\\\"]` disable healthcheck - `[\\\"CMD\\\", args...]` exec arguments directly - `[\\\"CMD-SHELL\\\", command]` run command with system's default shell */\n    Test?: string[];\n    /** The time to wait between checks in nanoseconds. It should be 0 or at least 1000000 (1 ms). 0 means inherit. */\n    Interval?: I64;\n    /** The time to wait before considering the check to have hung. It should be 0 or at least 1000000 (1 ms). 0 means inherit. */\n    Timeout?: I64;\n    /** The number of consecutive failures needed to consider a container as unhealthy. 0 means inherit. */\n    Retries?: I64;\n    /** Start period for the container to initialize before starting health-retries countdown in nanoseconds. It should be 0 or at least 1000000 (1 ms). 0 means inherit. */\n    StartPeriod?: I64;\n    /** The time to wait between checks in nanoseconds during the start period. It should be 0 or at least 1000000 (1 ms). 0 means inherit. */\n    StartInterval?: I64;\n}\n/** Configuration for a container that is portable between hosts.  When used as `ContainerConfig` field in an image, `ContainerConfig` is an optional field containing the configuration of the container that was last committed when creating the image.  Previous versions of Docker builder used this field to store build cache, and it is not in active use anymore. */\nexport interface ContainerConfig {\n    /** The hostname to use for the container, as a valid RFC 1123 hostname. */\n    Hostname?: string;\n    /** The domain name to use for the container. */\n    Domainname?: string;\n    /** The user that commands are run as inside the container. */\n    User?: string;\n    /** Whether to attach to `stdin`. */\n    AttachStdin?: boolean;\n    /** Whether to attach to `stdout`. */\n    AttachStdout?: boolean;\n    /** Whether to attach to `stderr`. */\n    AttachStderr?: boolean;\n    /** An object mapping ports to an empty object in the form:  `{\\\"<port>/<tcp|udp|sctp>\\\": {}}` */\n    ExposedPorts?: Record<string, Record<string, undefined>>;\n    /** Attach standard streams to a TTY, including `stdin` if it is not closed. */\n    Tty?: boolean;\n    /** Open `stdin` */\n    OpenStdin?: boolean;\n    /** Close `stdin` after one attached client disconnects */\n    StdinOnce?: boolean;\n    /** A list of environment variables to set inside the container in the form `[\\\"VAR=value\\\", ...]`. A variable without `=` is removed from the environment, rather than to have an empty value. */\n    Env?: string[];\n    /** Command to run specified as a string or an array of strings. */\n    Cmd?: string[];\n    Healthcheck?: HealthConfig;\n    /** Command is already escaped (Windows only) */\n    ArgsEscaped?: boolean;\n    /** The name (or reference) of the image to use when creating the container, or which was used when the container was created. */\n    Image?: string;\n    /** An object mapping mount point paths inside the container to empty objects. */\n    Volumes?: Record<string, Record<string, undefined>>;\n    /** The working directory for commands to run in. */\n    WorkingDir?: string;\n    /** The entry point for the container as a string or an array of strings.  If the array consists of exactly one empty string (`[\\\"\\\"]`) then the entry point is reset to system default (i.e., the entry point used by docker when there is no `ENTRYPOINT` instruction in the `Dockerfile`). */\n    Entrypoint?: string[];\n    /** Disable networking for the container. */\n    NetworkDisabled?: boolean;\n    /** MAC address of the container.  Deprecated: this field is deprecated in API v1.44 and up. Use EndpointSettings.MacAddress instead. */\n    MacAddress?: string;\n    /** `ONBUILD` metadata that were defined in the image's `Dockerfile`. */\n    OnBuild?: string[];\n    /** User-defined key/value metadata. */\n    Labels?: Record<string, string>;\n    /** Signal to stop a container as a string or unsigned integer. */\n    StopSignal?: string;\n    /** Timeout to stop a container in seconds. */\n    StopTimeout?: I64;\n    /** Shell for when `RUN`, `CMD`, and `ENTRYPOINT` uses a shell. */\n    Shell?: string[];\n}\n/** EndpointIPAMConfig represents an endpoint's IPAM configuration. */\nexport interface EndpointIpamConfig {\n    IPv4Address?: string;\n    IPv6Address?: string;\n    LinkLocalIPs?: string[];\n}\n/** Configuration for a network endpoint. */\nexport interface EndpointSettings {\n    IPAMConfig?: EndpointIpamConfig;\n    Links?: string[];\n    /** MAC address for the endpoint on this network. The network driver might ignore this parameter. */\n    MacAddress?: string;\n    Aliases?: string[];\n    /** Unique ID of the network. */\n    NetworkID?: string;\n    /** Unique ID for the service endpoint in a Sandbox. */\n    EndpointID?: string;\n    /** Gateway address for this network. */\n    Gateway?: string;\n    /** IPv4 address. */\n    IPAddress?: string;\n    /** Mask length of the IPv4 address. */\n    IPPrefixLen?: I64;\n    /** IPv6 gateway address. */\n    IPv6Gateway?: string;\n    /** Global IPv6 address. */\n    GlobalIPv6Address?: string;\n    /** Mask length of the global IPv6 address. */\n    GlobalIPv6PrefixLen?: I64;\n    /** DriverOpts is a mapping of driver options and values. These options are passed directly to the driver and are driver specific. */\n    DriverOpts?: Record<string, string>;\n    /** List of all DNS names an endpoint has on a specific network. This list is based on the container name, network aliases, container short ID, and hostname.  These DNS names are non-fully qualified but can contain several dots. You can get fully qualified DNS names by appending `.<network-name>`. For instance, if container name is `my.ctr` and the network is named `testnet`, `DNSNames` will contain `my.ctr` and the FQDN will be `my.ctr.testnet`. */\n    DNSNames?: string[];\n}\n/** NetworkSettings exposes the network settings in the API */\nexport interface NetworkSettings {\n    /** Name of the default bridge interface when dockerd's --bridge flag is set. */\n    Bridge?: string;\n    /** SandboxID uniquely represents a container's network stack. */\n    SandboxID?: string;\n    Ports?: Record<string, PortBinding[]>;\n    /** SandboxKey is the full path of the netns handle */\n    SandboxKey?: string;\n    /** Information about all networks that the container is connected to. */\n    Networks?: Record<string, EndpointSettings>;\n}\nexport interface Container {\n    /** The ID of the container */\n    Id?: string;\n    /** The time the container was created */\n    Created?: string;\n    /** The path to the command being run */\n    Path?: string;\n    /** The arguments to the command being run */\n    Args?: string[];\n    State?: ContainerState;\n    /** The container's image ID */\n    Image?: string;\n    ResolvConfPath?: string;\n    HostnamePath?: string;\n    HostsPath?: string;\n    LogPath?: string;\n    Name?: string;\n    RestartCount?: I64;\n    Driver?: string;\n    Platform?: string;\n    MountLabel?: string;\n    ProcessLabel?: string;\n    AppArmorProfile?: string;\n    /** IDs of exec instances that are running in the container. */\n    ExecIDs?: string[];\n    HostConfig?: HostConfig;\n    GraphDriver?: GraphDriverData;\n    /** The size of files that have been created or changed by this container. */\n    SizeRw?: I64;\n    /** The total size of all the files in this container. */\n    SizeRootFs?: I64;\n    Mounts?: MountPoint[];\n    Config?: ContainerConfig;\n    NetworkSettings?: NetworkSettings;\n}\nexport type InspectDeploymentContainerResponse = Container;\nexport type InspectDockerContainerResponse = Container;\n/** Information about the image's RootFS, including the layer IDs. */\nexport interface ImageInspectRootFs {\n    Type?: string;\n    Layers?: string[];\n}\n/** Additional metadata of the image in the local cache. This information is local to the daemon, and not part of the image itself. */\nexport interface ImageInspectMetadata {\n    /** Date and time at which the image was last tagged in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.  This information is only available if the image was tagged locally, and omitted otherwise. */\n    LastTagTime?: string;\n}\n/** Information about an image in the local image cache. */\nexport interface Image {\n    /** ID is the content-addressable ID of an image.  This identifier is a content-addressable digest calculated from the image's configuration (which includes the digests of layers used by the image).  Note that this digest differs from the `RepoDigests` below, which holds digests of image manifests that reference the image. */\n    Id?: string;\n    /** List of image names/tags in the local image cache that reference this image.  Multiple image tags can refer to the same image, and this list may be empty if no tags reference the image, in which case the image is \\\"untagged\\\", in which case it can still be referenced by its ID. */\n    RepoTags?: string[];\n    /** List of content-addressable digests of locally available image manifests that the image is referenced from. Multiple manifests can refer to the same image.  These digests are usually only available if the image was either pulled from a registry, or if the image was pushed to a registry, which is when the manifest is generated and its digest calculated. */\n    RepoDigests?: string[];\n    /** ID of the parent image.  Depending on how the image was created, this field may be empty and is only set for images that were built/created locally. This field is empty if the image was pulled from an image registry. */\n    Parent?: string;\n    /** Optional message that was set when committing or importing the image. */\n    Comment?: string;\n    /** Date and time at which the image was created, formatted in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.  This information is only available if present in the image, and omitted otherwise. */\n    Created?: string;\n    /** The version of Docker that was used to build the image.  Depending on how the image was created, this field may be empty. */\n    DockerVersion?: string;\n    /** Name of the author that was specified when committing the image, or as specified through MAINTAINER (deprecated) in the Dockerfile. */\n    Author?: string;\n    /** Configuration for a container that is portable between hosts. */\n    Config?: ContainerConfig;\n    /** Hardware CPU architecture that the image runs on. */\n    Architecture?: string;\n    /** CPU architecture variant (presently ARM-only). */\n    Variant?: string;\n    /** Operating System the image is built to run on. */\n    Os?: string;\n    /** Operating System version the image is built to run on (especially for Windows). */\n    OsVersion?: string;\n    /** Total size of the image including all layers it is composed of. */\n    Size?: I64;\n    GraphDriver?: GraphDriverData;\n    RootFS?: ImageInspectRootFs;\n    Metadata?: ImageInspectMetadata;\n}\nexport type InspectDockerImageResponse = Image;\nexport interface IpamConfig {\n    Subnet?: string;\n    IPRange?: string;\n    Gateway?: string;\n    AuxiliaryAddresses: Record<string, string>;\n}\nexport interface Ipam {\n    /** Name of the IPAM driver to use. */\n    Driver?: string;\n    /** List of IPAM configuration options, specified as a map:  ``` {\\\"Subnet\\\": <CIDR>, \\\"IPRange\\\": <CIDR>, \\\"Gateway\\\": <IP address>, \\\"AuxAddress\\\": <device_name:IP address>} ``` */\n    Config: IpamConfig[];\n    /** Driver-specific options, specified as a map. */\n    Options: Record<string, string>;\n}\nexport interface NetworkContainer {\n    /** This is the key on the incoming map of NetworkContainer */\n    ContainerID?: string;\n    Name?: string;\n    EndpointID?: string;\n    MacAddress?: string;\n    IPv4Address?: string;\n    IPv6Address?: string;\n}\nexport interface Network {\n    Name?: string;\n    Id?: string;\n    Created?: string;\n    Scope?: string;\n    Driver?: string;\n    EnableIPv6?: boolean;\n    IPAM?: Ipam;\n    Internal?: boolean;\n    Attachable?: boolean;\n    Ingress?: boolean;\n    /** This field is turned from map into array for easier usability. */\n    Containers: NetworkContainer[];\n    Options?: Record<string, string>;\n    Labels?: Record<string, string>;\n}\nexport type InspectDockerNetworkResponse = Network;\nexport declare enum VolumeScopeEnum {\n    Empty = \"\",\n    Local = \"local\",\n    Global = \"global\"\n}\nexport type U64 = number;\n/** The version number of the object such as node, service, etc. This is needed to avoid conflicting writes. The client must send the version number along with the modified specification when updating these objects.  This approach ensures safe concurrency and determinism in that the change on the object may not be applied if the version number has changed from the last read. In other words, if two update requests specify the same base version, only one of the requests can succeed. As a result, two separate update requests that happen at the same time will not unintentionally overwrite each other. */\nexport interface ObjectVersion {\n    Index?: U64;\n}\nexport declare enum ClusterVolumeSpecAccessModeScopeEnum {\n    Empty = \"\",\n    Single = \"single\",\n    Multi = \"multi\"\n}\nexport declare enum ClusterVolumeSpecAccessModeSharingEnum {\n    Empty = \"\",\n    None = \"none\",\n    Readonly = \"readonly\",\n    Onewriter = \"onewriter\",\n    All = \"all\"\n}\n/** One cluster volume secret entry. Defines a key-value pair that is passed to the plugin. */\nexport interface ClusterVolumeSpecAccessModeSecrets {\n    /** Key is the name of the key of the key-value pair passed to the plugin. */\n    Key?: string;\n    /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */\n    Secret?: string;\n}\nexport type Topology = Record<string, PortBinding[]>;\n/** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */\nexport interface ClusterVolumeSpecAccessModeAccessibilityRequirements {\n    /** A list of required topologies, at least one of which the volume must be accessible from. */\n    Requisite?: Topology[];\n    /** A list of topologies that the volume should attempt to be provisioned in. */\n    Preferred?: Topology[];\n}\n/** The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity. */\nexport interface ClusterVolumeSpecAccessModeCapacityRange {\n    /** The volume must be at least this big. The value of 0 indicates an unspecified minimum */\n    RequiredBytes?: I64;\n    /** The volume must not be bigger than this. The value of 0 indicates an unspecified maximum. */\n    LimitBytes?: I64;\n}\nexport declare enum ClusterVolumeSpecAccessModeAvailabilityEnum {\n    Empty = \"\",\n    Active = \"active\",\n    Pause = \"pause\",\n    Drain = \"drain\"\n}\n/** Defines how the volume is used by tasks. */\nexport interface ClusterVolumeSpecAccessMode {\n    /** The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time. */\n    Scope?: ClusterVolumeSpecAccessModeScopeEnum;\n    /** The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers. */\n    Sharing?: ClusterVolumeSpecAccessModeSharingEnum;\n    /** Swarm Secrets that are passed to the CSI storage plugin when operating on this volume. */\n    Secrets?: ClusterVolumeSpecAccessModeSecrets[];\n    AccessibilityRequirements?: ClusterVolumeSpecAccessModeAccessibilityRequirements;\n    CapacityRange?: ClusterVolumeSpecAccessModeCapacityRange;\n    /** The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. */\n    Availability?: ClusterVolumeSpecAccessModeAvailabilityEnum;\n}\n/** Cluster-specific options used to create the volume. */\nexport interface ClusterVolumeSpec {\n    /** Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services.  Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group. */\n    Group?: string;\n    AccessMode?: ClusterVolumeSpecAccessMode;\n}\n/** Information about the global status of the volume. */\nexport interface ClusterVolumeInfo {\n    /** The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown. */\n    CapacityBytes?: I64;\n    /** A map of strings to strings returned from the storage plugin when the volume is created. */\n    VolumeContext?: Record<string, string>;\n    /** The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet. */\n    VolumeID?: string;\n    /** The topology this volume is actually accessible from. */\n    AccessibleTopology?: Topology[];\n}\nexport declare enum ClusterVolumePublishStatusStateEnum {\n    Empty = \"\",\n    PendingPublish = \"pending-publish\",\n    Published = \"published\",\n    PendingNodeUnpublish = \"pending-node-unpublish\",\n    PendingControllerUnpublish = \"pending-controller-unpublish\"\n}\nexport interface ClusterVolumePublishStatus {\n    /** The ID of the Swarm node the volume is published on. */\n    NodeID?: string;\n    /** The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. */\n    State?: ClusterVolumePublishStatusStateEnum;\n    /** A map of strings to strings returned by the CSI controller plugin when a volume is published. */\n    PublishContext?: Record<string, string>;\n}\n/** Options and information specific to, and only present on, Swarm CSI cluster volumes. */\nexport interface ClusterVolume {\n    /** The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name. */\n    ID?: string;\n    Version?: ObjectVersion;\n    CreatedAt?: string;\n    UpdatedAt?: string;\n    Spec?: ClusterVolumeSpec;\n    Info?: ClusterVolumeInfo;\n    /** The status of the volume as it pertains to its publishing and use on specific nodes */\n    PublishStatus?: ClusterVolumePublishStatus[];\n}\n/** Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints. */\nexport interface VolumeUsageData {\n    /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\\\"local\\\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\\\"not available\\\") */\n    Size: I64;\n    /** The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available. */\n    RefCount: I64;\n}\nexport interface Volume {\n    /** Name of the volume. */\n    Name: string;\n    /** Name of the volume driver used by the volume. */\n    Driver: string;\n    /** Mount path of the volume on the host. */\n    Mountpoint: string;\n    /** Date/Time the volume was created. */\n    CreatedAt?: string;\n    /** Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\\\"key\\\":\\\"value\\\",\\\"key2\\\":\\\"value2\\\"}`.  The `Status` field is optional, and is omitted if the volume driver does not support this feature. */\n    Status?: Record<string, Record<string, undefined>>;\n    /** User-defined key/value metadata. */\n    Labels?: Record<string, string>;\n    /** The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. */\n    Scope?: VolumeScopeEnum;\n    ClusterVolume?: ClusterVolume;\n    /** The driver specific options used when creating the volume. */\n    Options?: Record<string, string>;\n    UsageData?: VolumeUsageData;\n}\nexport type InspectDockerVolumeResponse = Volume;\nexport type InspectStackContainerResponse = Container;\nexport type JsonObject = any;\nexport type JsonValue = any;\nexport type ListActionsResponse = ActionListItem[];\nexport type ListAlertersResponse = AlerterListItem[];\nexport declare enum PortTypeEnum {\n    EMPTY = \"\",\n    TCP = \"tcp\",\n    UDP = \"udp\",\n    SCTP = \"sctp\"\n}\n/** An open port on a container */\nexport interface Port {\n    /** Host IP address that the container's port is mapped to */\n    IP?: string;\n    /** Port on the container */\n    PrivatePort?: number;\n    /** Port exposed on the host */\n    PublicPort?: number;\n    Type?: PortTypeEnum;\n}\n/** Container summary returned by container list apis. */\nexport interface ContainerListItem {\n    /** The Server which holds the container. */\n    server_id?: string;\n    /** The first name in Names, not including the initial '/' */\n    name: string;\n    /** The ID of this container */\n    id?: string;\n    /** The name of the image used when creating this container */\n    image?: string;\n    /** The ID of the image that this container was created from */\n    image_id?: string;\n    /** When the container was created */\n    created?: I64;\n    /** The size of files that have been created or changed by this container */\n    size_rw?: I64;\n    /** The total size of all the files in this container */\n    size_root_fs?: I64;\n    /** The state of this container (e.g. `exited`) */\n    state: ContainerStateStatusEnum;\n    /** Additional human-readable status of this container (e.g. `Exit 0`) */\n    status?: string;\n    /** The network mode */\n    network_mode?: string;\n    /** The network names attached to container */\n    networks?: string[];\n    /** Port mappings for the container */\n    ports?: Port[];\n    /** The volume names attached to container */\n    volumes?: string[];\n    /** The container stats, if they can be retreived. */\n    stats?: ContainerStats;\n    /**\n     * The labels attached to container.\n     * It's too big to send with container list,\n     * can get it using InspectContainer\n     */\n    labels?: Record<string, string>;\n}\nexport type ListAllDockerContainersResponse = ContainerListItem[];\n/** An api key used to authenticate requests via request headers. */\nexport interface ApiKey {\n    /** Unique key associated with secret */\n    key: string;\n    /** Hash of the secret */\n    secret: string;\n    /** User associated with the api key */\n    user_id: string;\n    /** Name associated with the api key for management */\n    name: string;\n    /** Timestamp of key creation */\n    created_at: I64;\n    /** Expiry of key, or 0 if never expires */\n    expires: I64;\n}\nexport type ListApiKeysForServiceUserResponse = ApiKey[];\nexport type ListApiKeysResponse = ApiKey[];\nexport interface BuildVersionResponseItem {\n    version: Version;\n    ts: I64;\n}\nexport type ListBuildVersionsResponse = BuildVersionResponseItem[];\nexport type ListBuildersResponse = BuilderListItem[];\nexport type ListBuildsResponse = BuildListItem[];\nexport type ListCommonBuildExtraArgsResponse = string[];\nexport type ListCommonDeploymentExtraArgsResponse = string[];\nexport type ListCommonStackBuildExtraArgsResponse = string[];\nexport type ListCommonStackExtraArgsResponse = string[];\nexport interface ComposeProject {\n    /** The compose project name. */\n    name: string;\n    /** The status of the project, as returned by docker. */\n    status?: string;\n    /** The compose files included in the project. */\n    compose_files: string[];\n}\nexport type ListComposeProjectsResponse = ComposeProject[];\nexport type ListDeploymentsResponse = DeploymentListItem[];\nexport type ListDockerContainersResponse = ContainerListItem[];\n/** individual image layer information in response to ImageHistory operation */\nexport interface ImageHistoryResponseItem {\n    Id: string;\n    Created: I64;\n    CreatedBy: string;\n    Tags?: string[];\n    Size: I64;\n    Comment: string;\n}\nexport type ListDockerImageHistoryResponse = ImageHistoryResponseItem[];\nexport interface ImageListItem {\n    /** The first tag in `repo_tags`, or Id if no tags. */\n    name: string;\n    /** ID is the content-addressable ID of an image.  This identifier is a content-addressable digest calculated from the image's configuration (which includes the digests of layers used by the image).  Note that this digest differs from the `RepoDigests` below, which holds digests of image manifests that reference the image. */\n    id: string;\n    /** ID of the parent image.  Depending on how the image was created, this field may be empty and is only set for images that were built/created locally. This field is empty if the image was pulled from an image registry. */\n    parent_id: string;\n    /** Date and time at which the image was created as a Unix timestamp (number of seconds sinds EPOCH). */\n    created: I64;\n    /** Total size of the image including all layers it is composed of. */\n    size: I64;\n    /** Whether the image is in use by any container */\n    in_use: boolean;\n}\nexport type ListDockerImagesResponse = ImageListItem[];\nexport interface NetworkListItem {\n    name?: string;\n    id?: string;\n    created?: string;\n    scope?: string;\n    driver?: string;\n    enable_ipv6?: boolean;\n    ipam_driver?: string;\n    ipam_subnet?: string;\n    ipam_gateway?: string;\n    internal?: boolean;\n    attachable?: boolean;\n    ingress?: boolean;\n    /** Whether the network is attached to one or more containers */\n    in_use: boolean;\n}\nexport type ListDockerNetworksResponse = NetworkListItem[];\nexport interface ProviderAccount {\n    /** The account username. Required. */\n    username: string;\n    /** The account access token. Required. */\n    token?: string;\n}\nexport interface DockerRegistry {\n    /** The docker provider domain. Default: `docker.io`. */\n    domain: string;\n    /** The accounts on the registry. Required. */\n    accounts: ProviderAccount[];\n    /**\n     * Available organizations on the registry provider.\n     * Used to push an image under an organization's repo rather than an account's repo.\n     */\n    organizations?: string[];\n}\nexport type ListDockerRegistriesFromConfigResponse = DockerRegistry[];\nexport type ListDockerRegistryAccountsResponse = DockerRegistryAccount[];\nexport interface VolumeListItem {\n    /** The name of the volume */\n    name: string;\n    driver: string;\n    mountpoint: string;\n    created?: string;\n    scope: VolumeScopeEnum;\n    /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\\\"local\\\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\\\"not available\\\") */\n    size?: I64;\n    /** Whether the volume is currently attached to any container */\n    in_use: boolean;\n}\nexport type ListDockerVolumesResponse = VolumeListItem[];\nexport type ListFullActionsResponse = Action[];\nexport type ListFullAlertersResponse = Alerter[];\nexport type ListFullBuildersResponse = Builder[];\nexport type ListFullBuildsResponse = Build[];\nexport type ListFullDeploymentsResponse = Deployment[];\nexport type ListFullProceduresResponse = Procedure[];\nexport type ListFullReposResponse = Repo[];\nexport type ListFullResourceSyncsResponse = ResourceSync[];\nexport type ListFullServersResponse = Server[];\nexport type ListFullStacksResponse = Stack[];\nexport type ListGitProviderAccountsResponse = GitProviderAccount[];\nexport interface GitProvider {\n    /** The git provider domain. Default: `github.com`. */\n    domain: string;\n    /** Whether to use https. Default: true. */\n    https: boolean;\n    /** The accounts on the git provider. Required. */\n    accounts: ProviderAccount[];\n}\nexport type ListGitProvidersFromConfigResponse = GitProvider[];\nexport type UserTarget = \n/** User Id */\n{\n    type: \"User\";\n    id: string;\n}\n/** UserGroup Id */\n | {\n    type: \"UserGroup\";\n    id: string;\n};\n/** Representation of a User or UserGroups permission on a resource. */\nexport interface Permission {\n    /** The id of the permission document */\n    _id?: MongoId;\n    /** The target User / UserGroup */\n    user_target: UserTarget;\n    /** The target resource */\n    resource_target: ResourceTarget;\n    /** The permission level for the [user_target] on the [resource_target]. */\n    level?: PermissionLevel;\n    /** Any specific permissions for the [user_target] on the [resource_target]. */\n    specific?: Array<SpecificPermission>;\n}\nexport type ListPermissionsResponse = Permission[];\nexport declare enum ProcedureState {\n    /** Currently running */\n    Running = \"Running\",\n    /** Last run successful */\n    Ok = \"Ok\",\n    /** Last run failed */\n    Failed = \"Failed\",\n    /** Other case (never run) */\n    Unknown = \"Unknown\"\n}\nexport interface ProcedureListItemInfo {\n    /** Number of stages procedure has. */\n    stages: I64;\n    /** Reflect whether last run successful / currently running. */\n    state: ProcedureState;\n    /** Procedure last successful run timestamp in ms. */\n    last_run_at?: I64;\n    /**\n     * If the procedure has schedule enabled, this is the\n     * next scheduled run time in unix ms.\n     */\n    next_scheduled_run?: I64;\n    /**\n     * If there is an error parsing schedule expression,\n     * it will be given here.\n     */\n    schedule_error?: string;\n}\nexport type ProcedureListItem = ResourceListItem<ProcedureListItemInfo>;\nexport type ListProceduresResponse = ProcedureListItem[];\nexport declare enum RepoState {\n    /** Unknown case */\n    Unknown = \"Unknown\",\n    /** Last clone / pull successful (or never cloned) */\n    Ok = \"Ok\",\n    /** Last clone / pull failed */\n    Failed = \"Failed\",\n    /** Currently cloning */\n    Cloning = \"Cloning\",\n    /** Currently pulling */\n    Pulling = \"Pulling\",\n    /** Currently building */\n    Building = \"Building\"\n}\nexport interface RepoListItemInfo {\n    /** The server that repo sits on. */\n    server_id: string;\n    /** The builder that builds the repo. */\n    builder_id: string;\n    /** Repo last cloned / pulled timestamp in ms. */\n    last_pulled_at: I64;\n    /** Repo last built timestamp in ms. */\n    last_built_at: I64;\n    /** The git provider domain */\n    git_provider: string;\n    /** The configured repo */\n    repo: string;\n    /** The configured branch */\n    branch: string;\n    /** Full link to the repo. */\n    repo_link: string;\n    /** The repo state */\n    state: RepoState;\n    /** If the repo is cloned, will be the cloned short commit hash. */\n    cloned_hash?: string;\n    /** If the repo is cloned, will be the cloned commit message. */\n    cloned_message?: string;\n    /** If the repo is built, will be the latest built short commit hash. */\n    built_hash?: string;\n    /** Will be the latest remote short commit hash. */\n    latest_hash?: string;\n}\nexport type RepoListItem = ResourceListItem<RepoListItemInfo>;\nexport type ListReposResponse = RepoListItem[];\nexport declare enum ResourceSyncState {\n    /** Currently syncing */\n    Syncing = \"Syncing\",\n    /** Updates pending */\n    Pending = \"Pending\",\n    /** Last sync successful (or never synced). No Changes pending */\n    Ok = \"Ok\",\n    /** Last sync failed */\n    Failed = \"Failed\",\n    /** Other case */\n    Unknown = \"Unknown\"\n}\nexport interface ResourceSyncListItemInfo {\n    /** Unix timestamp of last sync, or 0 */\n    last_sync_ts: I64;\n    /** Whether sync is `files_on_host` mode. */\n    files_on_host: boolean;\n    /** Whether sync has file contents defined. */\n    file_contents: boolean;\n    /** Whether sync has `managed` mode enabled. */\n    managed: boolean;\n    /** Resource paths to the files. */\n    resource_path: string[];\n    /** Linked repo, if one is attached. */\n    linked_repo: string;\n    /** The git provider domain. */\n    git_provider: string;\n    /** The Github repo used as the source of the sync resources */\n    repo: string;\n    /** The branch of the repo */\n    branch: string;\n    /** Full link to the repo. */\n    repo_link: string;\n    /** Short commit hash of last sync, or empty string */\n    last_sync_hash?: string;\n    /** Commit message of last sync, or empty string */\n    last_sync_message?: string;\n    /** State of the sync. Reflects whether most recent sync successful. */\n    state: ResourceSyncState;\n}\nexport type ResourceSyncListItem = ResourceListItem<ResourceSyncListItemInfo>;\nexport type ListResourceSyncsResponse = ResourceSyncListItem[];\n/** A scheduled Action / Procedure run. */\nexport interface Schedule {\n    /** Procedure or Alerter */\n    target: ResourceTarget;\n    /** Readable name of the target resource */\n    name: string;\n    /** The format of the schedule expression */\n    schedule_format: ScheduleFormat;\n    /** The schedule for the run */\n    schedule: string;\n    /** Whether the scheduled run is enabled */\n    enabled: boolean;\n    /** Custom schedule timezone if it exists */\n    schedule_timezone: string;\n    /** Last run timestamp in ms. */\n    last_run_at?: I64;\n    /** Next scheduled run time in unix ms. */\n    next_scheduled_run?: I64;\n    /**\n     * If there is an error parsing schedule expression,\n     * it will be given here.\n     */\n    schedule_error?: string;\n    /** Resource tags. */\n    tags: string[];\n}\nexport type ListSchedulesResponse = Schedule[];\nexport type ListSecretsResponse = string[];\nexport declare enum ServerState {\n    /** Server health check passing. */\n    Ok = \"Ok\",\n    /** Server is unreachable. */\n    NotOk = \"NotOk\",\n    /** Server is disabled. */\n    Disabled = \"Disabled\"\n}\nexport interface ServerListItemInfo {\n    /** The server's state. */\n    state: ServerState;\n    /** Region of the server. */\n    region: string;\n    /** Address of the server. */\n    address: string;\n    /**\n     * External address of the server (reachable by users).\n     * Used with links.\n     */\n    external_address?: string;\n    /** The Komodo Periphery version of the server. */\n    version: string;\n    /** Whether server is configured to send unreachable alerts. */\n    send_unreachable_alerts: boolean;\n    /** Whether server is configured to send cpu alerts. */\n    send_cpu_alerts: boolean;\n    /** Whether server is configured to send mem alerts. */\n    send_mem_alerts: boolean;\n    /** Whether server is configured to send disk alerts. */\n    send_disk_alerts: boolean;\n    /** Whether server is configured to send version mismatch alerts. */\n    send_version_mismatch_alerts: boolean;\n    /** Whether terminals are disabled for this Server. */\n    terminals_disabled: boolean;\n    /** Whether container exec is disabled for this Server. */\n    container_exec_disabled: boolean;\n}\nexport type ServerListItem = ResourceListItem<ServerListItemInfo>;\nexport type ListServersResponse = ServerListItem[];\nexport interface StackService {\n    /** The service name */\n    service: string;\n    /** The service image */\n    image: string;\n    /** The container */\n    container?: ContainerListItem;\n    /** Whether there is an update available for this services image. */\n    update_available: boolean;\n}\nexport type ListStackServicesResponse = StackService[];\nexport declare enum StackState {\n    /** The stack is currently re/deploying */\n    Deploying = \"deploying\",\n    /** All containers are running. */\n    Running = \"running\",\n    /** All containers are paused */\n    Paused = \"paused\",\n    /** All contianers are stopped */\n    Stopped = \"stopped\",\n    /** All containers are created */\n    Created = \"created\",\n    /** All containers are restarting */\n    Restarting = \"restarting\",\n    /** All containers are dead */\n    Dead = \"dead\",\n    /** All containers are removing */\n    Removing = \"removing\",\n    /** The containers are in a mix of states */\n    Unhealthy = \"unhealthy\",\n    /** The stack is not deployed */\n    Down = \"down\",\n    /** Server not reachable for status */\n    Unknown = \"unknown\"\n}\nexport interface StackServiceWithUpdate {\n    service: string;\n    /** The service's image */\n    image: string;\n    /** Whether there is a newer image available for this service */\n    update_available: boolean;\n}\nexport interface StackListItemInfo {\n    /** The server that stack is deployed on. */\n    server_id: string;\n    /** Whether stack is using files on host mode */\n    files_on_host: boolean;\n    /** Whether stack has file contents defined. */\n    file_contents: boolean;\n    /** Linked repo, if one is attached. */\n    linked_repo: string;\n    /** The git provider domain */\n    git_provider: string;\n    /** The configured repo */\n    repo: string;\n    /** The configured branch */\n    branch: string;\n    /** Full link to the repo. */\n    repo_link: string;\n    /** The stack state */\n    state: StackState;\n    /** A string given by docker conveying the status of the stack. */\n    status?: string;\n    /**\n     * The services that are part of the stack.\n     * If deployed, will be `deployed_services`.\n     * Otherwise, its `latest_services`\n     */\n    services: StackServiceWithUpdate[];\n    /**\n     * Whether the compose project is missing on the host.\n     * Ie, it does not show up in `docker compose ls`.\n     * If true, and the stack is not Down, this is an unhealthy state.\n     */\n    project_missing: boolean;\n    /**\n     * If any compose files are missing in the repo, the path will be here.\n     * If there are paths here, this is an unhealthy state, and deploying will fail.\n     */\n    missing_files: string[];\n    /** Deployed short commit hash, or null. Only for repo based stacks. */\n    deployed_hash?: string;\n    /** Latest short commit hash, or null. Only for repo based stacks */\n    latest_hash?: string;\n}\nexport type StackListItem = ResourceListItem<StackListItemInfo>;\nexport type ListStacksResponse = StackListItem[];\n/** Information about a process on the system. */\nexport interface SystemProcess {\n    /** The process PID */\n    pid: number;\n    /** The process name */\n    name: string;\n    /** The path to the process executable */\n    exe?: string;\n    /** The command used to start the process */\n    cmd: string[];\n    /** The time the process was started */\n    start_time?: number;\n    /**\n     * The cpu usage percentage of the process.\n     * This is in core-percentage, eg 100% is 1 full core, and\n     * an 8 core machine would max at 800%.\n     */\n    cpu_perc: number;\n    /** The memory usage of the process in MB */\n    mem_mb: number;\n    /** Process disk read in KB/s */\n    disk_read_kb: number;\n    /** Process disk write in KB/s */\n    disk_write_kb: number;\n}\nexport type ListSystemProcessesResponse = SystemProcess[];\nexport type ListTagsResponse = Tag[];\n/**\n * Info about an active terminal on a server.\n * Retrieve with [ListTerminals][crate::api::read::server::ListTerminals].\n */\nexport interface TerminalInfo {\n    /** The name of the terminal. */\n    name: string;\n    /** The root program / args of the pty */\n    command: string;\n    /** The size of the terminal history in memory. */\n    stored_size_kb: number;\n}\nexport type ListTerminalsResponse = TerminalInfo[];\nexport type ListUserGroupsResponse = UserGroup[];\nexport type ListUserTargetPermissionsResponse = Permission[];\nexport type ListUsersResponse = User[];\nexport type ListVariablesResponse = Variable[];\n/** The response for [LoginLocalUser] */\nexport type LoginLocalUserResponse = JwtResponse;\nexport type MongoDocument = any;\nexport interface ProcedureQuerySpecifics {\n}\nexport type ProcedureQuery = ResourceQuery<ProcedureQuerySpecifics>;\nexport type PushRecentlyViewedResponse = NoData;\nexport interface RepoQuerySpecifics {\n    /** Filter repos by their repo. */\n    repos: string[];\n}\nexport type RepoQuery = ResourceQuery<RepoQuerySpecifics>;\nexport interface ResourceSyncQuerySpecifics {\n    /** Filter syncs by their repo. */\n    repos: string[];\n}\nexport type ResourceSyncQuery = ResourceQuery<ResourceSyncQuerySpecifics>;\nexport type SearchContainerLogResponse = Log;\nexport type SearchDeploymentLogResponse = Log;\nexport type SearchStackLogResponse = Log;\nexport interface ServerQuerySpecifics {\n}\n/** Server-specific query */\nexport type ServerQuery = ResourceQuery<ServerQuerySpecifics>;\nexport type SetLastSeenUpdateResponse = NoData;\n/** Response for [SignUpLocalUser]. */\nexport type SignUpLocalUserResponse = JwtResponse;\nexport interface StackQuerySpecifics {\n    /**\n     * Query only for Stacks on these Servers.\n     * If empty, does not filter by Server.\n     * Only accepts Server id (not name).\n     */\n    server_ids?: string[];\n    /**\n     * Query only for Stacks with these linked repos.\n     * Only accepts Repo id (not name).\n     */\n    linked_repos?: string[];\n    /** Filter syncs by their repo. */\n    repos?: string[];\n    /** Query only for Stack with available image updates. */\n    update_available?: boolean;\n}\nexport type StackQuery = ResourceQuery<StackQuerySpecifics>;\nexport type UpdateDockerRegistryAccountResponse = DockerRegistryAccount;\nexport type UpdateGitProviderAccountResponse = GitProviderAccount;\nexport type UpdatePermissionOnResourceTypeResponse = NoData;\nexport type UpdatePermissionOnTargetResponse = NoData;\nexport type UpdateProcedureResponse = Procedure;\nexport type UpdateResourceMetaResponse = NoData;\nexport type UpdateServiceUserDescriptionResponse = User;\nexport type UpdateUserAdminResponse = NoData;\nexport type UpdateUserBasePermissionsResponse = NoData;\nexport type UpdateUserPasswordResponse = NoData;\nexport type UpdateUserUsernameResponse = NoData;\nexport type UpdateVariableDescriptionResponse = Variable;\nexport type UpdateVariableIsSecretResponse = Variable;\nexport type UpdateVariableValueResponse = Variable;\nexport type _PartialActionConfig = Partial<ActionConfig>;\nexport type _PartialAlerterConfig = Partial<AlerterConfig>;\nexport type _PartialAwsBuilderConfig = Partial<AwsBuilderConfig>;\nexport type _PartialBuildConfig = Partial<BuildConfig>;\nexport type _PartialBuilderConfig = Partial<BuilderConfig>;\nexport type _PartialDeploymentConfig = Partial<DeploymentConfig>;\nexport type _PartialDockerRegistryAccount = Partial<DockerRegistryAccount>;\nexport type _PartialGitProviderAccount = Partial<GitProviderAccount>;\nexport type _PartialProcedureConfig = Partial<ProcedureConfig>;\nexport type _PartialRepoConfig = Partial<RepoConfig>;\nexport type _PartialResourceSyncConfig = Partial<ResourceSyncConfig>;\nexport type _PartialServerBuilderConfig = Partial<ServerBuilderConfig>;\nexport type _PartialServerConfig = Partial<ServerConfig>;\nexport type _PartialStackConfig = Partial<StackConfig>;\nexport type _PartialTag = Partial<Tag>;\nexport type _PartialUrlBuilderConfig = Partial<UrlBuilderConfig>;\nexport interface __Serror {\n    error: string;\n    trace: string[];\n}\nexport type _Serror = __Serror;\n/** **Admin only.** Add a user to a user group. Response: [UserGroup] */\nexport interface AddUserToUserGroup {\n    /** The name or id of UserGroup that user should be added to. */\n    user_group: string;\n    /** The id or username of the user to add */\n    user: string;\n}\n/** Configuration for an AWS builder. */\nexport interface AwsBuilderConfig {\n    /** The AWS region to create the instance in */\n    region: string;\n    /** The instance type to create for the build */\n    instance_type: string;\n    /** The size of the builder volume in gb */\n    volume_gb: number;\n    /**\n     * The port periphery will be running on.\n     * Default: `8120`\n     */\n    port: number;\n    use_https: boolean;\n    /**\n     * The EC2 ami id to create.\n     * The ami should have the periphery client configured to start on startup,\n     * and should have the necessary github / dockerhub accounts configured.\n     */\n    ami_id?: string;\n    /** The subnet id to create the instance in. */\n    subnet_id?: string;\n    /** The key pair name to attach to the instance */\n    key_pair_name?: string;\n    /**\n     * Whether to assign the instance a public IP address.\n     * Likely needed for the instance to be able to reach the open internet.\n     */\n    assign_public_ip?: boolean;\n    /**\n     * Whether core should use the public IP address to communicate with periphery on the builder.\n     * If false, core will communicate with the instance using the private IP.\n     */\n    use_public_ip?: boolean;\n    /**\n     * The security group ids to attach to the instance.\n     * This should include a security group to allow core inbound access to the periphery port.\n     */\n    security_group_ids?: string[];\n    /** The user data to deploy the instance with. */\n    user_data?: string;\n    /** Which git providers are available on the AMI */\n    git_providers?: GitProvider[];\n    /** Which docker registries are available on the AMI. */\n    docker_registries?: DockerRegistry[];\n    /** Which secrets are available on the AMI. */\n    secrets?: string[];\n}\n/**\n * Backs up the Komodo Core database to compressed jsonl files.\n * Admin only. Response: [Update]\n *\n * Mount a folder to `/backups`, and Core will use it to create\n * timestamped database dumps, which can be restored using\n * the Komodo CLI.\n *\n * https://komo.do/docs/setup/backup\n */\nexport interface BackupCoreDatabase {\n}\n/** Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchBuildRepo {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* repos\n     * foo-*\n     * # add some more\n     * extra-repo-1, extra-repo-2\n     * ```\n     */\n    pattern: string;\n}\n/** Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchCloneRepo {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* repos\n     * foo-*\n     * # add some more\n     * extra-repo-1, extra-repo-2\n     * ```\n     */\n    pattern: string;\n}\n/** Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchDeploy {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* deployments\n     * foo-*\n     * # add some more\n     * extra-deployment-1, extra-deployment-2\n     * ```\n     */\n    pattern: string;\n}\n/** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchDeployStack {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* stacks\n     * foo-*\n     * # add some more\n     * extra-stack-1, extra-stack-2\n     * ```\n     */\n    pattern: string;\n}\n/** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchDeployStackIfChanged {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* stacks\n     * foo-*\n     * # add some more\n     * extra-stack-1, extra-stack-2\n     * ```\n     */\n    pattern: string;\n}\n/** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchDestroyDeployment {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* deployments\n     * foo-*\n     * # add some more\n     * extra-deployment-1, extra-deployment-2\n     * ```\n     */\n    pattern: string;\n}\n/** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchDestroyStack {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     * d\n     * Example:\n     * ```text\n     * # match all foo-* stacks\n     * foo-*\n     * # add some more\n     * extra-stack-1, extra-stack-2\n     * ```\n     */\n    pattern: string;\n}\nexport interface BatchExecutionResponseItemErr {\n    name: string;\n    error: _Serror;\n}\n/** Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchPullRepo {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* repos\n     * foo-*\n     * # add some more\n     * extra-repo-1, extra-repo-2\n     * ```\n     */\n    pattern: string;\n}\n/** Pulls multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchPullStack {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* stacks\n     * foo-*\n     * # add some more\n     * extra-stack-1, extra-stack-2\n     * ```\n     */\n    pattern: string;\n}\n/** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse] */\nexport interface BatchRunAction {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* actions\n     * foo-*\n     * # add some more\n     * extra-action-1, extra-action-2\n     * ```\n     */\n    pattern: string;\n}\n/** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchRunBuild {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* builds\n     * foo-*\n     * # add some more\n     * extra-build-1, extra-build-2\n     * ```\n     */\n    pattern: string;\n}\n/** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse]. */\nexport interface BatchRunProcedure {\n    /**\n     * Id or name or wildcard pattern or regex.\n     * Supports multiline and comma delineated combinations of the above.\n     *\n     * Example:\n     * ```text\n     * # match all foo-* procedures\n     * foo-*\n     * # add some more\n     * extra-procedure-1, extra-procedure-2\n     * ```\n     */\n    pattern: string;\n}\n/**\n * Builds the target repo, using the attached builder. Response: [Update].\n *\n * Note. Repo must have builder attached at `builder_id`.\n *\n * 1. Spawns the target builder instance (For AWS type. For Server type, just use CloneRepo).\n * 2. Clones the repo on the builder using `git clone https://{$token?}@github.com/${repo} -b ${branch}`.\n * The token will only be used if a github account is specified,\n * and must be declared in the periphery configuration on the builder instance.\n * 3. If `on_clone` and `on_pull` are specified, they will be executed.\n * `on_clone` will be executed before `on_pull`.\n */\nexport interface BuildRepo {\n    /** Id or name */\n    repo: string;\n}\n/** Item in [GetBuildMonthlyStatsResponse] */\nexport interface BuildStatsDay {\n    time: number;\n    count: number;\n    ts: number;\n}\n/**\n * Cancels the target build.\n * Only does anything if the build is `building` when called.\n * Response: [Update]\n */\nexport interface CancelBuild {\n    /** Can be id or name */\n    build: string;\n}\n/**\n * Cancels the target repo build.\n * Only does anything if the repo build is `building` when called.\n * Response: [Update]\n */\nexport interface CancelRepoBuild {\n    /** Can be id or name */\n    repo: string;\n}\n/**\n * Clears all repos from the Core repo cache. Admin only.\n * Response: [Update]\n */\nexport interface ClearRepoCache {\n}\n/**\n * Clones the target repo. Response: [Update].\n *\n * Note. Repo must have server attached at `server_id`.\n *\n * 1. Clones the repo on the target server using `git clone https://{$token?}@github.com/${repo} -b ${branch}`.\n * The token will only be used if a github account is specified,\n * and must be declared in the periphery configuration on the target server.\n * 2. If `on_clone` and `on_pull` are specified, they will be executed.\n * `on_clone` will be executed before `on_pull`.\n */\nexport interface CloneRepo {\n    /** Id or name */\n    repo: string;\n}\n/**\n * Exports matching resources, and writes to the target sync's resource file. Response: [Update]\n *\n * Note. Will fail if the Sync is not `managed`.\n */\nexport interface CommitSync {\n    /** Id or name */\n    sync: string;\n}\n/**\n * Query to connect to a container exec session (interactive shell over websocket) on the given server.\n * TODO: Document calling.\n */\nexport interface ConnectContainerExecQuery {\n    /** Server Id or name */\n    server: string;\n    /** The container name */\n    container: string;\n    /** The shell to use (eg. `sh` or `bash`) */\n    shell: string;\n}\n/**\n * Query to connect to a container exec session (interactive shell over websocket) on the given Deployment.\n * This call will use access to the Deployment Terminal to permission the call.\n * TODO: Document calling.\n */\nexport interface ConnectDeploymentExecQuery {\n    /** Deployment Id or name */\n    deployment: string;\n    /** The shell to use (eg. `sh` or `bash`) */\n    shell: string;\n}\n/**\n * Query to connect to a container exec session (interactive shell over websocket) on the given Stack / service.\n * This call will use access to the Stack Terminal to permission the call.\n * TODO: Document calling.\n */\nexport interface ConnectStackExecQuery {\n    /** Stack Id or name */\n    stack: string;\n    /** The service name to connect to */\n    service: string;\n    /** The shell to use (eg. `sh` or `bash`) */\n    shell: string;\n}\n/**\n * Query to connect to a terminal (interactive shell over websocket) on the given server.\n * TODO: Document calling.\n */\nexport interface ConnectTerminalQuery {\n    /** Server Id or name */\n    server: string;\n    /**\n     * Each periphery can keep multiple terminals open.\n     * If a terminals with the specified name does not exist,\n     * the call will fail.\n     * Create a terminal using [CreateTerminal][super::write::server::CreateTerminal]\n     */\n    terminal: string;\n}\n/** Blkio stats entry.  This type is Linux-specific and omitted for Windows containers. */\nexport interface ContainerBlkioStatEntry {\n    major?: U64;\n    minor?: U64;\n    op?: string;\n    value?: U64;\n}\n/**\n * BlkioStats stores all IO service stats for data read and write.\n * This type is Linux-specific and holds many fields that are specific to cgroups v1.\n * On a cgroup v2 host, all fields other than `io_service_bytes_recursive` are omitted or `null`.\n * This type is only populated on Linux and omitted for Windows containers.\n */\nexport interface ContainerBlkioStats {\n    io_service_bytes_recursive?: ContainerBlkioStatEntry[];\n    /**\n     * This field is only available when using Linux containers with cgroups v1.\n     * It is omitted or `null` when using cgroups v2.\n     */\n    io_serviced_recursive?: ContainerBlkioStatEntry[];\n    /**\n     * This field is only available when using Linux containers with cgroups v1.\n     * It is omitted or `null` when using cgroups v2.\n     */\n    io_queue_recursive?: ContainerBlkioStatEntry[];\n    /**\n     * This field is only available when using Linux containers with cgroups v1.\n     * It is omitted or `null` when using cgroups v2.\n     */\n    io_service_time_recursive?: ContainerBlkioStatEntry[];\n    /**\n     * This field is only available when using Linux containers with cgroups v1.\n     * It is omitted or `null` when using cgroups v2.\n     */\n    io_wait_time_recursive?: ContainerBlkioStatEntry[];\n    /**\n     * This field is only available when using Linux containers with cgroups v1.\n     * It is omitted or `null` when using cgroups v2.\n     */\n    io_merged_recursive?: ContainerBlkioStatEntry[];\n    /**\n     * This field is only available when using Linux containers with cgroups v1.\n     * It is omitted or `null` when using cgroups v2.\n     */\n    io_time_recursive?: ContainerBlkioStatEntry[];\n    /**\n     * This field is only available when using Linux containers with cgroups v1.\n     * It is omitted or `null` when using cgroups v2.\n     */\n    sectors_recursive?: ContainerBlkioStatEntry[];\n}\n/** All CPU stats aggregated since container inception. */\nexport interface ContainerCpuUsage {\n    /** Total CPU time consumed in nanoseconds (Linux) or 100's of nanoseconds (Windows). */\n    total_usage?: U64;\n    /**\n     * Total CPU time (in nanoseconds) consumed per core (Linux).\n     * This field is Linux-specific when using cgroups v1.\n     * It is omitted when using cgroups v2 and Windows containers.\n     */\n    percpu_usage?: U64[];\n    /**\n     * Time (in nanoseconds) spent by tasks of the cgroup in kernel mode (Linux),\n     * or time spent (in 100's of nanoseconds) by all container processes in kernel mode (Windows).\n     * Not populated for Windows containers using Hyper-V isolation.\n     */\n    usage_in_kernelmode?: U64;\n    /**\n     * Time (in nanoseconds) spent by tasks of the cgroup in user mode (Linux),\n     * or time spent (in 100's of nanoseconds) by all container processes in kernel mode (Windows).\n     * Not populated for Windows containers using Hyper-V isolation.\n     */\n    usage_in_usermode?: U64;\n}\n/**\n * CPU throttling stats of the container.\n * This type is Linux-specific and omitted for Windows containers.\n */\nexport interface ContainerThrottlingData {\n    /** Number of periods with throttling active. */\n    periods?: U64;\n    /** Number of periods when the container hit its throttling limit. */\n    throttled_periods?: U64;\n    /** Aggregated time (in nanoseconds) the container was throttled for. */\n    throttled_time?: U64;\n}\n/** CPU related info of the container */\nexport interface ContainerCpuStats {\n    /** All CPU stats aggregated since container inception. */\n    cpu_usage?: ContainerCpuUsage;\n    /**\n     * System Usage.\n     * This field is Linux-specific and omitted for Windows containers.\n     */\n    system_cpu_usage?: U64;\n    /**\n     * Number of online CPUs.\n     * This field is Linux-specific and omitted for Windows containers.\n     */\n    online_cpus?: number;\n    /**\n     * CPU throttling stats of the container.\n     * This type is Linux-specific and omitted for Windows containers.\n     */\n    throttling_data?: ContainerThrottlingData;\n}\n/**\n * Aggregates all memory stats since container inception on Linux.\n * Windows returns stats for commit and private working set only.\n */\nexport interface ContainerMemoryStats {\n    /**\n     * Current `res_counter` usage for memory.\n     * This field is Linux-specific and omitted for Windows containers.\n     */\n    usage?: U64;\n    /**\n     * Maximum usage ever recorded.\n     * This field is Linux-specific and only supported on cgroups v1.\n     * It is omitted when using cgroups v2 and for Windows containers.\n     */\n    max_usage?: U64;\n    /**\n     * All the stats exported via memory.stat. when using cgroups v2.\n     * This field is Linux-specific and omitted for Windows containers.\n     */\n    stats?: Record<string, U64>;\n    /** Number of times memory usage hits limits.  This field is Linux-specific and only supported on cgroups v1. It is omitted when using cgroups v2 and for Windows containers. */\n    failcnt?: U64;\n    /** This field is Linux-specific and omitted for Windows containers. */\n    limit?: U64;\n    /**\n     * Committed bytes.\n     * This field is Windows-specific and omitted for Linux containers.\n     */\n    commitbytes?: U64;\n    /**\n     * Peak committed bytes.\n     * This field is Windows-specific and omitted for Linux containers.\n     */\n    commitpeakbytes?: U64;\n    /**\n     * Private working set.\n     * This field is Windows-specific and omitted for Linux containers.\n     */\n    privateworkingset?: U64;\n}\n/** Aggregates the network stats of one container */\nexport interface ContainerNetworkStats {\n    /** Bytes received. Windows and Linux. */\n    rx_bytes?: U64;\n    /** Packets received. Windows and Linux. */\n    rx_packets?: U64;\n    /**\n     * Received errors. Not used on Windows.\n     * This field is Linux-specific and always zero for Windows containers.\n     */\n    rx_errors?: U64;\n    /** Incoming packets dropped. Windows and Linux. */\n    rx_dropped?: U64;\n    /** Bytes sent. Windows and Linux. */\n    tx_bytes?: U64;\n    /** Packets sent. Windows and Linux. */\n    tx_packets?: U64;\n    /**\n     * Sent errors. Not used on Windows.\n     * This field is Linux-specific and always zero for Windows containers.\n     */\n    tx_errors?: U64;\n    /** Outgoing packets dropped. Windows and Linux. */\n    tx_dropped?: U64;\n    /**\n     * Endpoint ID. Not used on Linux.\n     * This field is Windows-specific and omitted for Linux containers.\n     */\n    endpoint_id?: string;\n    /**\n     * Instance ID. Not used on Linux.\n     * This field is Windows-specific and omitted for Linux containers.\n     */\n    instance_id?: string;\n}\n/** PidsStats contains Linux-specific stats of a container's process-IDs (PIDs).  This type is Linux-specific and omitted for Windows containers. */\nexport interface ContainerPidsStats {\n    /** Current is the number of PIDs in the cgroup. */\n    current?: U64;\n    /** Limit is the hard limit on the number of pids in the cgroup. A \\\"Limit\\\" of 0 means that there is no limit. */\n    limit?: U64;\n}\n/**\n * StorageStats is the disk I/O stats for read/write on Windows.\n * This type is Windows-specific and omitted for Linux containers.\n */\nexport interface ContainerStorageStats {\n    read_count_normalized?: U64;\n    read_size_bytes?: U64;\n    write_count_normalized?: U64;\n    write_size_bytes?: U64;\n}\nexport interface Conversion {\n    /** reference on the server. */\n    local: string;\n    /** reference in the container. */\n    container: string;\n}\n/**\n * Creates a new action with given `name` and the configuration\n * of the action at the given `id`. Response: [Action].\n */\nexport interface CopyAction {\n    /** The name of the new action. */\n    name: string;\n    /** The id of the action to copy. */\n    id: string;\n}\n/**\n * Creates a new alerter with given `name` and the configuration\n * of the alerter at the given `id`. Response: [Alerter].\n */\nexport interface CopyAlerter {\n    /** The name of the new alerter. */\n    name: string;\n    /** The id of the alerter to copy. */\n    id: string;\n}\n/**\n * Creates a new build with given `name` and the configuration\n * of the build at the given `id`. Response: [Build].\n */\nexport interface CopyBuild {\n    /** The name of the new build. */\n    name: string;\n    /** The id of the build to copy. */\n    id: string;\n}\n/**\n * Creates a new builder with given `name` and the configuration\n * of the builder at the given `id`. Response: [Builder]\n */\nexport interface CopyBuilder {\n    /** The name of the new builder. */\n    name: string;\n    /** The id of the builder to copy. */\n    id: string;\n}\n/**\n * Creates a new deployment with given `name` and the configuration\n * of the deployment at the given `id`. Response: [Deployment]\n */\nexport interface CopyDeployment {\n    /** The name of the new deployment. */\n    name: string;\n    /** The id of the deployment to copy. */\n    id: string;\n}\n/**\n * Creates a new procedure with given `name` and the configuration\n * of the procedure at the given `id`. Response: [Procedure].\n */\nexport interface CopyProcedure {\n    /** The name of the new procedure. */\n    name: string;\n    /** The id of the procedure to copy. */\n    id: string;\n}\n/**\n * Creates a new repo with given `name` and the configuration\n * of the repo at the given `id`. Response: [Repo].\n */\nexport interface CopyRepo {\n    /** The name of the new repo. */\n    name: string;\n    /** The id of the repo to copy. */\n    id: string;\n}\n/**\n * Creates a new sync with given `name` and the configuration\n * of the sync at the given `id`. Response: [ResourceSync].\n */\nexport interface CopyResourceSync {\n    /** The name of the new sync. */\n    name: string;\n    /** The id of the sync to copy. */\n    id: string;\n}\n/**\n * Creates a new server with given `name` and the configuration\n * of the server at the given `id`. Response: [Server].\n */\nexport interface CopyServer {\n    /** The name of the new server. */\n    name: string;\n    /** The id of the server to copy. */\n    id: string;\n}\n/**\n * Creates a new stack with given `name` and the configuration\n * of the stack at the given `id`. Response: [Stack].\n */\nexport interface CopyStack {\n    /** The name of the new stack. */\n    name: string;\n    /** The id of the stack to copy. */\n    id: string;\n}\n/** Create a action. Response: [Action]. */\nexport interface CreateAction {\n    /** The name given to newly created action. */\n    name: string;\n    /** Optional partial config to initialize the action with. */\n    config?: _PartialActionConfig;\n}\n/**\n * Create a webhook on the github action attached to the Action resource.\n * passed in request. Response: [CreateActionWebhookResponse]\n */\nexport interface CreateActionWebhook {\n    /** Id or name */\n    action: string;\n}\n/** Create an alerter. Response: [Alerter]. */\nexport interface CreateAlerter {\n    /** The name given to newly created alerter. */\n    name: string;\n    /** Optional partial config to initialize the alerter with. */\n    config?: _PartialAlerterConfig;\n}\n/**\n * Create an api key for the calling user.\n * Response: [CreateApiKeyResponse].\n *\n * Note. After the response is served, there will be no way\n * to get the secret later.\n */\nexport interface CreateApiKey {\n    /** The name for the api key. */\n    name: string;\n    /**\n     * A unix timestamp in millseconds specifying api key expire time.\n     * Default is 0, which means no expiry.\n     */\n    expires?: I64;\n}\n/**\n * Admin only method to create an api key for a service user.\n * Response: [CreateApiKeyResponse].\n */\nexport interface CreateApiKeyForServiceUser {\n    /** Must be service user */\n    user_id: string;\n    /** The name for the api key */\n    name: string;\n    /**\n     * A unix timestamp in millseconds specifying api key expire time.\n     * Default is 0, which means no expiry.\n     */\n    expires?: I64;\n}\n/** Create a build. Response: [Build]. */\nexport interface CreateBuild {\n    /** The name given to newly created build. */\n    name: string;\n    /** Optional partial config to initialize the build with. */\n    config?: _PartialBuildConfig;\n}\n/**\n * Create a webhook on the github repo attached to the build\n * passed in request. Response: [CreateBuildWebhookResponse]\n */\nexport interface CreateBuildWebhook {\n    /** Id or name */\n    build: string;\n}\n/** Partial representation of [BuilderConfig] */\nexport type PartialBuilderConfig = {\n    type: \"Url\";\n    params: _PartialUrlBuilderConfig;\n} | {\n    type: \"Server\";\n    params: _PartialServerBuilderConfig;\n} | {\n    type: \"Aws\";\n    params: _PartialAwsBuilderConfig;\n};\n/** Create a builder. Response: [Builder]. */\nexport interface CreateBuilder {\n    /** The name given to newly created builder. */\n    name: string;\n    /** Optional partial config to initialize the builder with. */\n    config?: PartialBuilderConfig;\n}\n/** Create a deployment. Response: [Deployment]. */\nexport interface CreateDeployment {\n    /** The name given to newly created deployment. */\n    name: string;\n    /** Optional partial config to initialize the deployment with. */\n    config?: _PartialDeploymentConfig;\n}\n/** Create a Deployment from an existing container. Response: [Deployment]. */\nexport interface CreateDeploymentFromContainer {\n    /** The name or id of the existing container. */\n    name: string;\n    /** The server id or name on which container exists. */\n    server: string;\n}\n/**\n * **Admin only.** Create a docker registry account.\n * Response: [DockerRegistryAccount].\n */\nexport interface CreateDockerRegistryAccount {\n    account: _PartialDockerRegistryAccount;\n}\n/**\n * **Admin only.** Create a git provider account.\n * Response: [GitProviderAccount].\n */\nexport interface CreateGitProviderAccount {\n    /**\n     * The initial account config. Anything in the _id field will be ignored,\n     * as this is generated on creation.\n     */\n    account: _PartialGitProviderAccount;\n}\n/**\n * **Admin only.** Create a local user.\n * Response: [User].\n *\n * Note. Not to be confused with /auth/SignUpLocalUser.\n * This method requires admin user credentials, and can\n * bypass disabled user registration.\n */\nexport interface CreateLocalUser {\n    /** The username for the local user. */\n    username: string;\n    /** A password for the local user. */\n    password: string;\n}\n/**\n * Create a docker network on the server.\n * Response: [Update]\n *\n * `docker network create {name}`\n */\nexport interface CreateNetwork {\n    /** Server Id or name */\n    server: string;\n    /** The name of the network to create. */\n    name: string;\n}\n/** Create a procedure. Response: [Procedure]. */\nexport interface CreateProcedure {\n    /** The name given to newly created build. */\n    name: string;\n    /** Optional partial config to initialize the procedure with. */\n    config?: _PartialProcedureConfig;\n}\n/** Create a repo. Response: [Repo]. */\nexport interface CreateRepo {\n    /** The name given to newly created repo. */\n    name: string;\n    /** Optional partial config to initialize the repo with. */\n    config?: _PartialRepoConfig;\n}\nexport declare enum RepoWebhookAction {\n    Clone = \"Clone\",\n    Pull = \"Pull\",\n    Build = \"Build\"\n}\n/**\n * Create a webhook on the github repo attached to the (Komodo) Repo resource.\n * passed in request. Response: [CreateRepoWebhookResponse]\n */\nexport interface CreateRepoWebhook {\n    /** Id or name */\n    repo: string;\n    /** \"Clone\" or \"Pull\" or \"Build\" */\n    action: RepoWebhookAction;\n}\n/** Create a sync. Response: [ResourceSync]. */\nexport interface CreateResourceSync {\n    /** The name given to newly created sync. */\n    name: string;\n    /** Optional partial config to initialize the sync with. */\n    config?: _PartialResourceSyncConfig;\n}\n/** Create a server. Response: [Server]. */\nexport interface CreateServer {\n    /** The name given to newly created server. */\n    name: string;\n    /** Optional partial config to initialize the server with. */\n    config?: _PartialServerConfig;\n}\n/**\n * **Admin only.** Create a service user.\n * Response: [User].\n */\nexport interface CreateServiceUser {\n    /** The username for the service user. */\n    username: string;\n    /** A description for the service user. */\n    description: string;\n}\n/** Create a stack. Response: [Stack]. */\nexport interface CreateStack {\n    /** The name given to newly created stack. */\n    name: string;\n    /** Optional partial config to initialize the stack with. */\n    config?: _PartialStackConfig;\n}\nexport declare enum StackWebhookAction {\n    Refresh = \"Refresh\",\n    Deploy = \"Deploy\"\n}\n/**\n * Create a webhook on the github repo attached to the stack\n * passed in request. Response: [CreateStackWebhookResponse]\n */\nexport interface CreateStackWebhook {\n    /** Id or name */\n    stack: string;\n    /** \"Refresh\" or \"Deploy\" */\n    action: StackWebhookAction;\n}\nexport declare enum SyncWebhookAction {\n    Refresh = \"Refresh\",\n    Sync = \"Sync\"\n}\n/**\n * Create a webhook on the github repo attached to the sync\n * passed in request. Response: [CreateSyncWebhookResponse]\n */\nexport interface CreateSyncWebhook {\n    /** Id or name */\n    sync: string;\n    /** \"Refresh\" or \"Sync\" */\n    action: SyncWebhookAction;\n}\n/** Create a tag. Response: [Tag]. */\nexport interface CreateTag {\n    /** The name of the tag. */\n    name: string;\n    /** Tag color. Default: Slate. */\n    color?: TagColor;\n}\n/**\n * Configures the behavior of [CreateTerminal] if the\n * specified terminal name already exists.\n */\nexport declare enum TerminalRecreateMode {\n    /**\n     * Never kill the old terminal if it already exists.\n     * If the command is different, returns error.\n     */\n    Never = \"Never\",\n    /** Always kill the old terminal and create new one */\n    Always = \"Always\",\n    /** Only kill and recreate if the command is different. */\n    DifferentCommand = \"DifferentCommand\"\n}\n/**\n * Create a terminal on the server.\n * Response: [NoData]\n */\nexport interface CreateTerminal {\n    /** Server Id or name */\n    server: string;\n    /** The name of the terminal on the server to create. */\n    name: string;\n    /**\n     * The shell command (eg `bash`) to init the shell.\n     *\n     * This can also include args:\n     * `docker exec -it container sh`\n     *\n     * Default: `bash`\n     */\n    command: string;\n    /** Default: `Never` */\n    recreate?: TerminalRecreateMode;\n}\n/** **Admin only.** Create a user group. Response: [UserGroup] */\nexport interface CreateUserGroup {\n    /** The name to assign to the new UserGroup */\n    name: string;\n}\n/** **Admin only.** Create variable. Response: [Variable]. */\nexport interface CreateVariable {\n    /** The name of the variable to create. */\n    name: string;\n    /** The initial value of the variable. defualt: \"\". */\n    value?: string;\n    /** The initial value of the description. default: \"\". */\n    description?: string;\n    /** Whether to make this a secret variable. */\n    is_secret?: boolean;\n}\n/** Configuration for a Custom alerter endpoint. */\nexport interface CustomAlerterEndpoint {\n    /** The http/s endpoint to send the POST to */\n    url: string;\n}\n/**\n * Deletes the action at the given id, and returns the deleted action.\n * Response: [Action]\n */\nexport interface DeleteAction {\n    /** The id or name of the action to delete. */\n    id: string;\n}\n/**\n * Delete the webhook on the github action attached to the Action resource.\n * passed in request. Response: [DeleteActionWebhookResponse]\n */\nexport interface DeleteActionWebhook {\n    /** Id or name */\n    action: string;\n}\n/**\n * Deletes the alerter at the given id, and returns the deleted alerter.\n * Response: [Alerter]\n */\nexport interface DeleteAlerter {\n    /** The id or name of the alerter to delete. */\n    id: string;\n}\n/**\n * Delete all terminals on the server.\n * Response: [NoData]\n */\nexport interface DeleteAllTerminals {\n    /** Server Id or name */\n    server: string;\n}\n/**\n * Delete an api key for the calling user.\n * Response: [NoData]\n */\nexport interface DeleteApiKey {\n    /** The key which the user intends to delete. */\n    key: string;\n}\n/**\n * Admin only method to delete an api key for a service user.\n * Response: [NoData].\n */\nexport interface DeleteApiKeyForServiceUser {\n    key: string;\n}\n/**\n * Deletes the build at the given id, and returns the deleted build.\n * Response: [Build]\n */\nexport interface DeleteBuild {\n    /** The id or name of the build to delete. */\n    id: string;\n}\n/**\n * Delete a webhook on the github repo attached to the build\n * passed in request. Response: [CreateBuildWebhookResponse]\n */\nexport interface DeleteBuildWebhook {\n    /** Id or name */\n    build: string;\n}\n/**\n * Deletes the builder at the given id, and returns the deleted builder.\n * Response: [Builder]\n */\nexport interface DeleteBuilder {\n    /** The id or name of the builder to delete. */\n    id: string;\n}\n/**\n * Deletes the deployment at the given id, and returns the deleted deployment.\n * Response: [Deployment].\n *\n * Note. If the associated container is running, it will be deleted as part of\n * the deployment clean up.\n */\nexport interface DeleteDeployment {\n    /** The id or name of the deployment to delete. */\n    id: string;\n}\n/**\n * **Admin only.** Delete a docker registry account.\n * Response: [DockerRegistryAccount].\n */\nexport interface DeleteDockerRegistryAccount {\n    /** The id of the docker registry account to delete */\n    id: string;\n}\n/**\n * **Admin only.** Delete a git provider account.\n * Response: [DeleteGitProviderAccountResponse].\n */\nexport interface DeleteGitProviderAccount {\n    /** The id of the git provider to delete */\n    id: string;\n}\n/**\n * Delete a docker image.\n * Response: [Update]\n */\nexport interface DeleteImage {\n    /** Id or name. */\n    server: string;\n    /** The name of the image to delete. */\n    name: string;\n}\n/**\n * Delete a docker network.\n * Response: [Update]\n */\nexport interface DeleteNetwork {\n    /** Id or name. */\n    server: string;\n    /** The name of the network to delete. */\n    name: string;\n}\n/**\n * Deletes the procedure at the given id, and returns the deleted procedure.\n * Response: [Procedure]\n */\nexport interface DeleteProcedure {\n    /** The id or name of the procedure to delete. */\n    id: string;\n}\n/**\n * Deletes the repo at the given id, and returns the deleted repo.\n * Response: [Repo]\n */\nexport interface DeleteRepo {\n    /** The id or name of the repo to delete. */\n    id: string;\n}\n/**\n * Delete the webhook on the github repo attached to the (Komodo) Repo resource.\n * passed in request. Response: [DeleteRepoWebhookResponse]\n */\nexport interface DeleteRepoWebhook {\n    /** Id or name */\n    repo: string;\n    /** \"Clone\" or \"Pull\" or \"Build\" */\n    action: RepoWebhookAction;\n}\n/**\n * Deletes the sync at the given id, and returns the deleted sync.\n * Response: [ResourceSync]\n */\nexport interface DeleteResourceSync {\n    /** The id or name of the sync to delete. */\n    id: string;\n}\n/**\n * Deletes the server at the given id, and returns the deleted server.\n * Response: [Server]\n */\nexport interface DeleteServer {\n    /** The id or name of the server to delete. */\n    id: string;\n}\n/**\n * Deletes the stack at the given id, and returns the deleted stack.\n * Response: [Stack]\n */\nexport interface DeleteStack {\n    /** The id or name of the stack to delete. */\n    id: string;\n}\n/**\n * Delete the webhook on the github repo attached to the stack\n * passed in request. Response: [DeleteStackWebhookResponse]\n */\nexport interface DeleteStackWebhook {\n    /** Id or name */\n    stack: string;\n    /** \"Refresh\" or \"Deploy\" */\n    action: StackWebhookAction;\n}\n/**\n * Delete the webhook on the github repo attached to the sync\n * passed in request. Response: [DeleteSyncWebhookResponse]\n */\nexport interface DeleteSyncWebhook {\n    /** Id or name */\n    sync: string;\n    /** \"Refresh\" or \"Sync\" */\n    action: SyncWebhookAction;\n}\n/**\n * Delete a tag, and return the deleted tag. Response: [Tag].\n *\n * Note. Will also remove this tag from all attached resources.\n */\nexport interface DeleteTag {\n    /** The id of the tag to delete. */\n    id: string;\n}\n/**\n * Delete a terminal on the server.\n * Response: [NoData]\n */\nexport interface DeleteTerminal {\n    /** Server Id or name */\n    server: string;\n    /** The name of the terminal on the server to delete. */\n    terminal: string;\n}\n/**\n * **Admin only**. Delete a user.\n * Admins can delete any non-admin user.\n * Only Super Admin can delete an admin.\n * No users can delete a Super Admin user.\n * User cannot delete themselves.\n * Response: [NoData].\n */\nexport interface DeleteUser {\n    /** User id or username */\n    user: string;\n}\n/** **Admin only.** Delete a user group. Response: [UserGroup] */\nexport interface DeleteUserGroup {\n    /** The id of the UserGroup */\n    id: string;\n}\n/** **Admin only.** Delete a variable. Response: [Variable]. */\nexport interface DeleteVariable {\n    name: string;\n}\n/**\n * Delete a docker volume.\n * Response: [Update]\n */\nexport interface DeleteVolume {\n    /** Id or name. */\n    server: string;\n    /** The name of the volume to delete. */\n    name: string;\n}\n/**\n * Deploys the container for the target deployment. Response: [Update].\n *\n * 1. Pulls the image onto the target server.\n * 2. If the container is already running,\n * it will be stopped and removed using `docker container rm ${container_name}`.\n * 3. The container will be run using `docker run {...params}`,\n * where params are determined by the deployment's configuration.\n */\nexport interface Deploy {\n    /** Name or id */\n    deployment: string;\n    /**\n     * Override the default termination signal specified in the deployment.\n     * Only used when deployment needs to be taken down before redeploy.\n     */\n    stop_signal?: TerminationSignal;\n    /**\n     * Override the default termination max time.\n     * Only used when deployment needs to be taken down before redeploy.\n     */\n    stop_time?: number;\n}\n/** Deploys the target stack. `docker compose up`. Response: [Update] */\nexport interface DeployStack {\n    /** Id or name */\n    stack: string;\n    /**\n     * Filter to only deploy specific services.\n     * If empty, will deploy all services.\n     */\n    services?: string[];\n    /**\n     * Override the default termination max time.\n     * Only used if the stack needs to be taken down first.\n     */\n    stop_time?: number;\n}\n/**\n * Checks deployed contents vs latest contents,\n * and only if any changes found\n * will `docker compose up`. Response: [Update]\n */\nexport interface DeployStackIfChanged {\n    /** Id or name */\n    stack: string;\n    /**\n     * Override the default termination max time.\n     * Only used if the stack needs to be taken down first.\n     */\n    stop_time?: number;\n}\n/**\n * Stops and destroys the container on the target server.\n * Reponse: [Update].\n *\n * 1. The container is stopped and removed using `docker container rm ${container_name}`.\n */\nexport interface DestroyContainer {\n    /** Name or id */\n    server: string;\n    /** The container name */\n    container: string;\n    /** Override the default termination signal. */\n    signal?: TerminationSignal;\n    /** Override the default termination max time. */\n    time?: number;\n}\n/**\n * Stops and destroys the container for the target deployment.\n * Reponse: [Update].\n *\n * 1. The container is stopped and removed using `docker container rm ${container_name}`.\n */\nexport interface DestroyDeployment {\n    /** Name or id. */\n    deployment: string;\n    /** Override the default termination signal specified in the deployment. */\n    signal?: TerminationSignal;\n    /** Override the default termination max time. */\n    time?: number;\n}\n/** Destoys the target stack. `docker compose down`. Response: [Update] */\nexport interface DestroyStack {\n    /** Id or name */\n    stack: string;\n    /**\n     * Filter to only destroy specific services.\n     * If empty, will destroy all services.\n     */\n    services?: string[];\n    /** Pass `--remove-orphans` */\n    remove_orphans?: boolean;\n    /** Override the default termination max time. */\n    stop_time?: number;\n}\n/** Configuration for a Discord alerter. */\nexport interface DiscordAlerterEndpoint {\n    /** The Discord webhook url */\n    url: string;\n}\nexport interface EnvironmentVar {\n    variable: string;\n    value: string;\n}\n/**\n * Exchange a single use exchange token (safe for transport in url query)\n * for a jwt.\n * Response: [ExchangeForJwtResponse].\n */\nexport interface ExchangeForJwt {\n    /** The 'exchange token' */\n    token: string;\n}\n/**\n * Execute a command in the given containers shell.\n * TODO: Document calling.\n */\nexport interface ExecuteContainerExecBody {\n    /** Server Id or name */\n    server: string;\n    /** The container name */\n    container: string;\n    /** The shell to use (eg. `sh` or `bash`) */\n    shell: string;\n    /** The command to execute. */\n    command: string;\n}\n/**\n * Execute a command in the given containers shell.\n * TODO: Document calling.\n */\nexport interface ExecuteDeploymentExecBody {\n    /** Deployment Id or name */\n    deployment: string;\n    /** The shell to use (eg. `sh` or `bash`) */\n    shell: string;\n    /** The command to execute. */\n    command: string;\n}\n/**\n * Execute a command in the given containers shell.\n * TODO: Document calling.\n */\nexport interface ExecuteStackExecBody {\n    /** Stack Id or name */\n    stack: string;\n    /** The service name to connect to */\n    service: string;\n    /** The shell to use (eg. `sh` or `bash`) */\n    shell: string;\n    /** The command to execute. */\n    command: string;\n}\n/**\n * Execute a terminal command on the given server.\n * TODO: Document calling.\n */\nexport interface ExecuteTerminalBody {\n    /** Server Id or name */\n    server: string;\n    /**\n     * The name of the terminal on the server to use to execute.\n     * If the terminal at name exists, it will be used to execute the command.\n     * Otherwise, a new terminal will be created for this command, which will\n     * persist until it exits or is deleted.\n     */\n    terminal: string;\n    /** The command to execute. */\n    command: string;\n}\n/**\n * Get pretty formatted monrun sync toml for all resources\n * which the user has permissions to view.\n * Response: [TomlResponse].\n */\nexport interface ExportAllResourcesToToml {\n    /**\n     * Whether to include any resources (servers, stacks, etc.)\n     * in the exported contents.\n     * Default: `true`\n     */\n    include_resources: boolean;\n    /**\n     * Filter resources by tag.\n     * Accepts tag name or id. Empty array will not filter by tag.\n     */\n    tags?: string[];\n    /**\n     * Whether to include variables in the exported contents.\n     * Default: false\n     */\n    include_variables?: boolean;\n    /**\n     * Whether to include user groups in the exported contents.\n     * Default: false\n     */\n    include_user_groups?: boolean;\n}\n/**\n * Get pretty formatted monrun sync toml for specific resources and user groups.\n * Response: [TomlResponse].\n */\nexport interface ExportResourcesToToml {\n    /** The targets to include in the export. */\n    targets?: ResourceTarget[];\n    /** The user group names or ids to include in the export. */\n    user_groups?: string[];\n    /** Whether to include variables */\n    include_variables?: boolean;\n}\n/**\n * **Admin only.**\n * Find a user.\n * Response: [FindUserResponse]\n */\nexport interface FindUser {\n    /** Id or username */\n    user: string;\n}\n/** Statistics sample for a container. */\nexport interface FullContainerStats {\n    /** Name of the container */\n    name: string;\n    /** ID of the container */\n    id?: string;\n    /**\n     * Date and time at which this sample was collected.\n     * The value is formatted as [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) with nano-seconds.\n     */\n    read?: string;\n    /**\n     * Date and time at which this first sample was collected.\n     * This field is not propagated if the \\\"one-shot\\\" option is set.\n     * If the \\\"one-shot\\\" option is set, this field may be omitted, empty,\n     * or set to a default date (`0001-01-01T00:00:00Z`).\n     * The value is formatted as [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) with nano-seconds.\n     */\n    preread?: string;\n    /**\n     * PidsStats contains Linux-specific stats of a container's process-IDs (PIDs).\n     * This type is Linux-specific and omitted for Windows containers.\n     */\n    pids_stats?: ContainerPidsStats;\n    /**\n     * BlkioStats stores all IO service stats for data read and write.\n     * This type is Linux-specific and holds many fields that are specific to cgroups v1.\n     * On a cgroup v2 host, all fields other than `io_service_bytes_recursive` are omitted or `null`.\n     * This type is only populated on Linux and omitted for Windows containers.\n     */\n    blkio_stats?: ContainerBlkioStats;\n    /**\n     * The number of processors on the system.\n     * This field is Windows-specific and always zero for Linux containers.\n     */\n    num_procs?: number;\n    storage_stats?: ContainerStorageStats;\n    cpu_stats?: ContainerCpuStats;\n    precpu_stats?: ContainerCpuStats;\n    memory_stats?: ContainerMemoryStats;\n    /** Network statistics for the container per interface.  This field is omitted if the container has no networking enabled. */\n    networks?: Record<string, ContainerNetworkStats>;\n}\n/** Get a specific action. Response: [Action]. */\nexport interface GetAction {\n    /** Id or name */\n    action: string;\n}\n/** Get current action state for the action. Response: [ActionActionState]. */\nexport interface GetActionActionState {\n    /** Id or name */\n    action: string;\n}\n/**\n * Gets a summary of data relating to all actions.\n * Response: [GetActionsSummaryResponse].\n */\nexport interface GetActionsSummary {\n}\n/** Response for [GetActionsSummary]. */\nexport interface GetActionsSummaryResponse {\n    /** The total number of actions. */\n    total: number;\n    /** The number of actions with Ok state. */\n    ok: number;\n    /** The number of actions currently running. */\n    running: number;\n    /** The number of actions with failed state. */\n    failed: number;\n    /** The number of actions with unknown state. */\n    unknown: number;\n}\n/** Get an alert: Response: [Alert]. */\nexport interface GetAlert {\n    id: string;\n}\n/** Get a specific alerter. Response: [Alerter]. */\nexport interface GetAlerter {\n    /** Id or name */\n    alerter: string;\n}\n/**\n * Gets a summary of data relating to all alerters.\n * Response: [GetAlertersSummaryResponse].\n */\nexport interface GetAlertersSummary {\n}\n/** Response for [GetAlertersSummary]. */\nexport interface GetAlertersSummaryResponse {\n    total: number;\n}\n/** Get a specific build. Response: [Build]. */\nexport interface GetBuild {\n    /** Id or name */\n    build: string;\n}\n/** Get current action state for the build. Response: [BuildActionState]. */\nexport interface GetBuildActionState {\n    /** Id or name */\n    build: string;\n}\n/**\n * Gets summary and timeseries breakdown of the last months build count / time for charting.\n * Response: [GetBuildMonthlyStatsResponse].\n *\n * Note. This method is paginated. One page is 30 days of data.\n * Query for older pages by incrementing the page, starting at 0.\n */\nexport interface GetBuildMonthlyStats {\n    /**\n     * Query for older data by incrementing the page.\n     * `page: 0` is the default, and will return the most recent data.\n     */\n    page?: number;\n}\n/** Response for [GetBuildMonthlyStats]. */\nexport interface GetBuildMonthlyStatsResponse {\n    total_time: number;\n    total_count: number;\n    days: BuildStatsDay[];\n}\n/** Get whether a Build's target repo has a webhook for the build configured. Response: [GetBuildWebhookEnabledResponse]. */\nexport interface GetBuildWebhookEnabled {\n    /** Id or name */\n    build: string;\n}\n/** Response for [GetBuildWebhookEnabled] */\nexport interface GetBuildWebhookEnabledResponse {\n    /**\n     * Whether the repo webhooks can even be managed.\n     * The repo owner must be in `github_webhook_app.owners` list to be managed.\n     */\n    managed: boolean;\n    /** Whether pushes to branch trigger build. Will always be false if managed is false. */\n    enabled: boolean;\n}\n/** Get a specific builder by id or name. Response: [Builder]. */\nexport interface GetBuilder {\n    /** Id or name */\n    builder: string;\n}\n/**\n * Gets a summary of data relating to all builders.\n * Response: [GetBuildersSummaryResponse].\n */\nexport interface GetBuildersSummary {\n}\n/** Response for [GetBuildersSummary]. */\nexport interface GetBuildersSummaryResponse {\n    /** The total number of builders. */\n    total: number;\n}\n/**\n * Gets a summary of data relating to all builds.\n * Response: [GetBuildsSummaryResponse].\n */\nexport interface GetBuildsSummary {\n}\n/** Response for [GetBuildsSummary]. */\nexport interface GetBuildsSummaryResponse {\n    /** The total number of builds in Komodo. */\n    total: number;\n    /** The number of builds with Ok state. */\n    ok: number;\n    /** The number of builds with Failed state. */\n    failed: number;\n    /** The number of builds currently building. */\n    building: number;\n    /** The number of builds with unknown state. */\n    unknown: number;\n}\n/**\n * Get the container log's tail, split by stdout/stderr.\n * Response: [Log].\n *\n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface GetContainerLog {\n    /** Id or name */\n    server: string;\n    /** The container name */\n    container: string;\n    /**\n     * The number of lines of the log tail to include.\n     * Default: 100.\n     * Max: 5000.\n     */\n    tail: U64;\n    /** Enable `--timestamps` */\n    timestamps?: boolean;\n}\n/**\n * Get info about the core api configuration.\n * Response: [GetCoreInfoResponse].\n */\nexport interface GetCoreInfo {\n}\n/** Response for [GetCoreInfo]. */\nexport interface GetCoreInfoResponse {\n    /** The title assigned to this core api. */\n    title: string;\n    /** The monitoring interval of this core api. */\n    monitoring_interval: Timelength;\n    /** The webhook base url. */\n    webhook_base_url: string;\n    /** Whether transparent mode is enabled, which gives all users read access to all resources. */\n    transparent_mode: boolean;\n    /** Whether UI write access should be disabled */\n    ui_write_disabled: boolean;\n    /** Whether non admins can create resources */\n    disable_non_admin_create: boolean;\n    /** Whether confirm dialog should be disabled */\n    disable_confirm_dialog: boolean;\n    /** The repo owners for which github webhook management api is available */\n    github_webhook_owners: string[];\n    /** Whether to disable websocket automatic reconnect. */\n    disable_websocket_reconnect: boolean;\n    /** Whether to enable fancy toml highlighting. */\n    enable_fancy_toml: boolean;\n    /** TZ identifier Core is using, if manually set. */\n    timezone: string;\n}\n/** Get a specific deployment by name or id. Response: [Deployment]. */\nexport interface GetDeployment {\n    /** Id or name */\n    deployment: string;\n}\n/**\n * Get current action state for the deployment.\n * Response: [DeploymentActionState].\n */\nexport interface GetDeploymentActionState {\n    /** Id or name */\n    deployment: string;\n}\n/**\n * Get the container, including image / status, of the target deployment.\n * Response: [GetDeploymentContainerResponse].\n *\n * Note. This does not hit the server directly. The status comes from an\n * in memory cache on the core, which hits the server periodically\n * to keep it up to date.\n */\nexport interface GetDeploymentContainer {\n    /** Id or name */\n    deployment: string;\n}\n/** Response for [GetDeploymentContainer]. */\nexport interface GetDeploymentContainerResponse {\n    state: DeploymentState;\n    container?: ContainerListItem;\n}\n/**\n * Get the deployment log's tail, split by stdout/stderr.\n * Response: [Log].\n *\n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface GetDeploymentLog {\n    /** Id or name */\n    deployment: string;\n    /**\n     * The number of lines of the log tail to include.\n     * Default: 100.\n     * Max: 5000.\n     */\n    tail: U64;\n    /** Enable `--timestamps` */\n    timestamps?: boolean;\n}\n/**\n * Get the deployment container's stats using `docker stats`.\n * Response: [GetDeploymentStatsResponse].\n *\n * Note. This call will hit the underlying server directly for most up to date stats.\n */\nexport interface GetDeploymentStats {\n    /** Id or name */\n    deployment: string;\n}\n/**\n * Gets a summary of data relating to all deployments.\n * Response: [GetDeploymentsSummaryResponse].\n */\nexport interface GetDeploymentsSummary {\n}\n/** Response for [GetDeploymentsSummary]. */\nexport interface GetDeploymentsSummaryResponse {\n    /** The total number of Deployments */\n    total: I64;\n    /** The number of Deployments with Running state */\n    running: I64;\n    /** The number of Deployments with Stopped or Paused state */\n    stopped: I64;\n    /** The number of Deployments with NotDeployed state */\n    not_deployed: I64;\n    /** The number of Deployments with Restarting or Dead or Created (other) state */\n    unhealthy: I64;\n    /** The number of Deployments with Unknown state */\n    unknown: I64;\n}\n/**\n * Gets a summary of data relating to all containers.\n * Response: [GetDockerContainersSummaryResponse].\n */\nexport interface GetDockerContainersSummary {\n}\n/** Response for [GetDockerContainersSummary] */\nexport interface GetDockerContainersSummaryResponse {\n    /** The total number of Containers */\n    total: number;\n    /** The number of Containers with Running state */\n    running: number;\n    /** The number of Containers with Stopped or Paused or Created state */\n    stopped: number;\n    /** The number of Containers with Restarting or Dead state */\n    unhealthy: number;\n    /** The number of Containers with Unknown state */\n    unknown: number;\n}\n/**\n * Get a specific docker registry account.\n * Response: [GetDockerRegistryAccountResponse].\n */\nexport interface GetDockerRegistryAccount {\n    id: string;\n}\n/**\n * Get a specific git provider account.\n * Response: [GetGitProviderAccountResponse].\n */\nexport interface GetGitProviderAccount {\n    id: string;\n}\n/**\n * Paginated endpoint serving historical (timeseries) server stats for graphing.\n * Response: [GetHistoricalServerStatsResponse].\n */\nexport interface GetHistoricalServerStats {\n    /** Id or name */\n    server: string;\n    /** The granularity of the data. */\n    granularity: Timelength;\n    /**\n     * Page of historical data. Default is 0, which is the most recent data.\n     * Use with the `next_page` field of the response.\n     */\n    page?: number;\n}\n/** System stats stored on the database. */\nexport interface SystemStatsRecord {\n    /** Unix timestamp in milliseconds */\n    ts: I64;\n    /** Server id */\n    sid: string;\n    /** Cpu usage percentage */\n    cpu_perc: number;\n    /** Load average (1m, 5m, 15m) */\n    load_average?: SystemLoadAverage;\n    /** Memory used in GB */\n    mem_used_gb: number;\n    /** Total memory in GB */\n    mem_total_gb: number;\n    /** Disk used in GB */\n    disk_used_gb: number;\n    /** Total disk size in GB */\n    disk_total_gb: number;\n    /** Breakdown of individual disks, including their usage, total size, and mount point */\n    disks: SingleDiskUsage[];\n    /** Total network ingress in bytes */\n    network_ingress_bytes?: number;\n    /** Total network egress in bytes */\n    network_egress_bytes?: number;\n}\n/** Response to [GetHistoricalServerStats]. */\nexport interface GetHistoricalServerStatsResponse {\n    /** The timeseries page of data. */\n    stats: SystemStatsRecord[];\n    /** If there is a next page of data, pass this to `page` to get it. */\n    next_page?: number;\n}\n/**\n * Non authenticated route to see the available options\n * users have to login to Komodo, eg. local auth, github, google.\n * Response: [GetLoginOptionsResponse].\n */\nexport interface GetLoginOptions {\n}\n/** The response for [GetLoginOptions]. */\nexport interface GetLoginOptionsResponse {\n    /** Whether local auth is enabled. */\n    local: boolean;\n    /** Whether github login is enabled. */\n    github: boolean;\n    /** Whether google login is enabled. */\n    google: boolean;\n    /** Whether OIDC login is enabled. */\n    oidc: boolean;\n    /** Whether user registration (Sign Up) has been disabled */\n    registration_disabled: boolean;\n}\n/**\n * Get the version of the Komodo Periphery agent on the target server.\n * Response: [GetPeripheryVersionResponse].\n */\nexport interface GetPeripheryVersion {\n    /** Id or name */\n    server: string;\n}\n/** Response for [GetPeripheryVersion]. */\nexport interface GetPeripheryVersionResponse {\n    /** The version of periphery. */\n    version: string;\n}\n/**\n * Gets the calling user's permission level on a specific resource.\n * Factors in any UserGroup's permissions they may be a part of.\n * Response: [PermissionLevel]\n */\nexport interface GetPermission {\n    /** The target to get user permission on. */\n    target: ResourceTarget;\n}\n/** Get a specific procedure. Response: [Procedure]. */\nexport interface GetProcedure {\n    /** Id or name */\n    procedure: string;\n}\n/** Get current action state for the procedure. Response: [ProcedureActionState]. */\nexport interface GetProcedureActionState {\n    /** Id or name */\n    procedure: string;\n}\n/**\n * Gets a summary of data relating to all procedures.\n * Response: [GetProceduresSummaryResponse].\n */\nexport interface GetProceduresSummary {\n}\n/** Response for [GetProceduresSummary]. */\nexport interface GetProceduresSummaryResponse {\n    /** The total number of procedures. */\n    total: number;\n    /** The number of procedures with Ok state. */\n    ok: number;\n    /** The number of procedures currently running. */\n    running: number;\n    /** The number of procedures with failed state. */\n    failed: number;\n    /** The number of procedures with unknown state. */\n    unknown: number;\n}\n/** Get a specific repo. Response: [Repo]. */\nexport interface GetRepo {\n    /** Id or name */\n    repo: string;\n}\n/** Get current action state for the repo. Response: [RepoActionState]. */\nexport interface GetRepoActionState {\n    /** Id or name */\n    repo: string;\n}\n/** Get a target Repo's configured webhooks. Response: [GetRepoWebhooksEnabledResponse]. */\nexport interface GetRepoWebhooksEnabled {\n    /** Id or name */\n    repo: string;\n}\n/** Response for [GetRepoWebhooksEnabled] */\nexport interface GetRepoWebhooksEnabledResponse {\n    /**\n     * Whether the repo webhooks can even be managed.\n     * The repo owner must be in `github_webhook_app.owners` list to be managed.\n     */\n    managed: boolean;\n    /** Whether pushes to branch trigger clone. Will always be false if managed is false. */\n    clone_enabled: boolean;\n    /** Whether pushes to branch trigger pull. Will always be false if managed is false. */\n    pull_enabled: boolean;\n    /** Whether pushes to branch trigger build. Will always be false if managed is false. */\n    build_enabled: boolean;\n}\n/**\n * Gets a summary of data relating to all repos.\n * Response: [GetReposSummaryResponse].\n */\nexport interface GetReposSummary {\n}\n/** Response for [GetReposSummary] */\nexport interface GetReposSummaryResponse {\n    /** The total number of repos */\n    total: number;\n    /** The number of repos with Ok state. */\n    ok: number;\n    /** The number of repos currently cloning. */\n    cloning: number;\n    /** The number of repos currently pulling. */\n    pulling: number;\n    /** The number of repos currently building. */\n    building: number;\n    /** The number of repos with failed state. */\n    failed: number;\n    /** The number of repos with unknown state. */\n    unknown: number;\n}\n/** Find the attached resource for a container. Either Deployment or Stack. Response: [GetResourceMatchingContainerResponse]. */\nexport interface GetResourceMatchingContainer {\n    /** Id or name */\n    server: string;\n    /** The container name */\n    container: string;\n}\n/** Response for [GetResourceMatchingContainer]. Resource is either Deployment, Stack, or None. */\nexport interface GetResourceMatchingContainerResponse {\n    resource?: ResourceTarget;\n}\n/** Get a specific sync. Response: [ResourceSync]. */\nexport interface GetResourceSync {\n    /** Id or name */\n    sync: string;\n}\n/** Get current action state for the sync. Response: [ResourceSyncActionState]. */\nexport interface GetResourceSyncActionState {\n    /** Id or name */\n    sync: string;\n}\n/**\n * Gets a summary of data relating to all syncs.\n * Response: [GetResourceSyncsSummaryResponse].\n */\nexport interface GetResourceSyncsSummary {\n}\n/** Response for [GetResourceSyncsSummary] */\nexport interface GetResourceSyncsSummaryResponse {\n    /** The total number of syncs */\n    total: number;\n    /** The number of syncs with Ok state. */\n    ok: number;\n    /** The number of syncs currently syncing. */\n    syncing: number;\n    /** The number of syncs with pending updates */\n    pending: number;\n    /** The number of syncs with failed state. */\n    failed: number;\n    /** The number of syncs with unknown state. */\n    unknown: number;\n}\n/** Get a specific server. Response: [Server]. */\nexport interface GetServer {\n    /** Id or name */\n    server: string;\n}\n/** Get current action state for the servers. Response: [ServerActionState]. */\nexport interface GetServerActionState {\n    /** Id or name */\n    server: string;\n}\n/** Get the state of the target server. Response: [GetServerStateResponse]. */\nexport interface GetServerState {\n    /** Id or name */\n    server: string;\n}\n/** The response for [GetServerState]. */\nexport interface GetServerStateResponse {\n    /** The server status. */\n    status: ServerState;\n}\n/**\n * Gets a summary of data relating to all servers.\n * Response: [GetServersSummaryResponse].\n */\nexport interface GetServersSummary {\n}\n/** Response for [GetServersSummary]. */\nexport interface GetServersSummaryResponse {\n    /** The total number of servers. */\n    total: I64;\n    /** The number of healthy (`status: OK`) servers. */\n    healthy: I64;\n    /** The number of servers with warnings (e.g., version mismatch). */\n    warning: I64;\n    /** The number of unhealthy servers. */\n    unhealthy: I64;\n    /** The number of disabled servers. */\n    disabled: I64;\n}\n/** Get a specific stack. Response: [Stack]. */\nexport interface GetStack {\n    /** Id or name */\n    stack: string;\n}\n/** Get current action state for the stack. Response: [StackActionState]. */\nexport interface GetStackActionState {\n    /** Id or name */\n    stack: string;\n}\n/**\n * Get a stack's logs. Filter down included services. Response: [GetStackLogResponse].\n *\n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface GetStackLog {\n    /** Id or name */\n    stack: string;\n    /**\n     * Filter the logs to only ones from specific services.\n     * If empty, will include logs from all services.\n     */\n    services: string[];\n    /**\n     * The number of lines of the log tail to include.\n     * Default: 100.\n     * Max: 5000.\n     */\n    tail: U64;\n    /** Enable `--timestamps` */\n    timestamps?: boolean;\n}\n/** Get a target stack's configured webhooks. Response: [GetStackWebhooksEnabledResponse]. */\nexport interface GetStackWebhooksEnabled {\n    /** Id or name */\n    stack: string;\n}\n/** Response for [GetStackWebhooksEnabled] */\nexport interface GetStackWebhooksEnabledResponse {\n    /**\n     * Whether the repo webhooks can even be managed.\n     * The repo owner must be in `github_webhook_app.owners` list to be managed.\n     */\n    managed: boolean;\n    /** Whether pushes to branch trigger refresh. Will always be false if managed is false. */\n    refresh_enabled: boolean;\n    /** Whether pushes to branch trigger stack execution. Will always be false if managed is false. */\n    deploy_enabled: boolean;\n}\n/**\n * Gets a summary of data relating to all syncs.\n * Response: [GetStacksSummaryResponse].\n */\nexport interface GetStacksSummary {\n}\n/** Response for [GetStacksSummary] */\nexport interface GetStacksSummaryResponse {\n    /** The total number of stacks */\n    total: number;\n    /** The number of stacks with Running state. */\n    running: number;\n    /** The number of stacks with Stopped or Paused state. */\n    stopped: number;\n    /** The number of stacks with Down state. */\n    down: number;\n    /** The number of stacks with Unhealthy or Restarting or Dead or Created or Removing state. */\n    unhealthy: number;\n    /** The number of stacks with Unknown state. */\n    unknown: number;\n}\n/** Get a target Sync's configured webhooks. Response: [GetSyncWebhooksEnabledResponse]. */\nexport interface GetSyncWebhooksEnabled {\n    /** Id or name */\n    sync: string;\n}\n/** Response for [GetSyncWebhooksEnabled] */\nexport interface GetSyncWebhooksEnabledResponse {\n    /**\n     * Whether the repo webhooks can even be managed.\n     * The repo owner must be in `github_webhook_app.owners` list to be managed.\n     */\n    managed: boolean;\n    /** Whether pushes to branch trigger refresh. Will always be false if managed is false. */\n    refresh_enabled: boolean;\n    /** Whether pushes to branch trigger sync execution. Will always be false if managed is false. */\n    sync_enabled: boolean;\n}\n/**\n * Get the system information of the target server.\n * Response: [SystemInformation].\n */\nexport interface GetSystemInformation {\n    /** Id or name */\n    server: string;\n}\n/**\n * Get the system stats on the target server. Response: [SystemStats].\n *\n * Note. This does not hit the server directly. The stats come from an\n * in memory cache on the core, which hits the server periodically\n * to keep it up to date.\n */\nexport interface GetSystemStats {\n    /** Id or name */\n    server: string;\n}\n/** Get data for a specific tag. Response [Tag]. */\nexport interface GetTag {\n    /** Id or name */\n    tag: string;\n}\n/**\n * Get all data for the target update.\n * Response: [Update].\n */\nexport interface GetUpdate {\n    /** The update id. */\n    id: string;\n}\n/**\n * Get the user extracted from the request headers.\n * Response: [User].\n */\nexport interface GetUser {\n}\n/**\n * Get a specific user group by name or id.\n * Response: [UserGroup].\n */\nexport interface GetUserGroup {\n    /** Name or Id */\n    user_group: string;\n}\n/**\n * Gets the username of a specific user.\n * Response: [GetUsernameResponse]\n */\nexport interface GetUsername {\n    /** The id of the user. */\n    user_id: string;\n}\n/** Response for [GetUsername]. */\nexport interface GetUsernameResponse {\n    /** The username of the user. */\n    username: string;\n    /** An optional icon for the user. */\n    avatar?: string;\n}\n/**\n * List all available global variables.\n * Response: [Variable]\n *\n * Note. For non admin users making this call,\n * secret variables will have their values obscured.\n */\nexport interface GetVariable {\n    /** The name of the variable to get. */\n    name: string;\n}\n/**\n * Get the version of the Komodo Core api.\n * Response: [GetVersionResponse].\n */\nexport interface GetVersion {\n}\n/** Response for [GetVersion]. */\nexport interface GetVersionResponse {\n    /** The version of the core api. */\n    version: string;\n}\n/**\n * Trigger a global poll for image updates on Stacks and Deployments\n * with `poll_for_updates` or `auto_update` enabled.\n * Admin only. Response: [Update]\n *\n * 1. `docker compose pull` any Stacks / Deployments with `poll_for_updates` or `auto_update` enabled. This will pick up any available updates.\n * 2. Redeploy Stacks / Deployments that have updates found and 'auto_update' enabled.\n */\nexport interface GlobalAutoUpdate {\n}\n/**\n * Inspect the docker container associated with the Deployment.\n * Response: [Container].\n */\nexport interface InspectDeploymentContainer {\n    /** Id or name */\n    deployment: string;\n}\n/** Inspect a docker container on the server. Response: [Container]. */\nexport interface InspectDockerContainer {\n    /** Id or name */\n    server: string;\n    /** The container name */\n    container: string;\n}\n/** Inspect a docker image on the server. Response: [Image]. */\nexport interface InspectDockerImage {\n    /** Id or name */\n    server: string;\n    /** The image name */\n    image: string;\n}\n/** Inspect a docker network on the server. Response: [InspectDockerNetworkResponse]. */\nexport interface InspectDockerNetwork {\n    /** Id or name */\n    server: string;\n    /** The network name */\n    network: string;\n}\n/** Inspect a docker volume on the server. Response: [Volume]. */\nexport interface InspectDockerVolume {\n    /** Id or name */\n    server: string;\n    /** The volume name */\n    volume: string;\n}\n/**\n * Inspect the docker container associated with the Stack.\n * Response: [Container].\n */\nexport interface InspectStackContainer {\n    /** Id or name */\n    stack: string;\n    /** The service name to inspect */\n    service: string;\n}\nexport interface LatestCommit {\n    hash: string;\n    message: string;\n}\n/** List actions matching optional query. Response: [ListActionsResponse]. */\nexport interface ListActions {\n    /** optional structured query to filter actions. */\n    query?: ActionQuery;\n}\n/** List alerters matching optional query. Response: [ListAlertersResponse]. */\nexport interface ListAlerters {\n    /** Structured query to filter alerters. */\n    query?: AlerterQuery;\n}\n/**\n * Get a paginated list of alerts sorted by timestamp descending.\n * Response: [ListAlertsResponse].\n */\nexport interface ListAlerts {\n    /**\n     * Pass a custom mongo query to filter the alerts.\n     *\n     * ## Example JSON\n     * ```json\n     * {\n     * \"resolved\": \"false\",\n     * \"level\": \"CRITICAL\",\n     * \"$or\": [\n     * {\n     * \"target\": {\n     * \"type\": \"Server\",\n     * \"id\": \"6608bf89cb2a12b257ab6c09\"\n     * }\n     * },\n     * {\n     * \"target\": {\n     * \"type\": \"Server\",\n     * \"id\": \"660a5f60b74f90d5dae45fa3\"\n     * }\n     * }\n     * ]\n     * }\n     * ```\n     * This will filter to only include open alerts that have CRITICAL level on those two servers.\n     */\n    query?: MongoDocument;\n    /**\n     * Retrieve older results by incrementing the page.\n     * `page: 0` is default, and returns the most recent results.\n     */\n    page?: U64;\n}\n/** Response for [ListAlerts]. */\nexport interface ListAlertsResponse {\n    alerts: Alert[];\n    /**\n     * If more alerts exist, the next page will be given here.\n     * Otherwise it will be `null`\n     */\n    next_page?: I64;\n}\n/**\n * List all docker containers on the target server.\n * Response: [ListDockerContainersResponse].\n */\nexport interface ListAllDockerContainers {\n    /** Filter by server id or name. */\n    servers?: string[];\n}\n/**\n * Gets list of api keys for the calling user.\n * Response: [ListApiKeysResponse]\n */\nexport interface ListApiKeys {\n}\n/**\n * **Admin only.**\n * Gets list of api keys for the user.\n * Will still fail if you call for a user_id that isn't a service user.\n * Response: [ListApiKeysForServiceUserResponse]\n */\nexport interface ListApiKeysForServiceUser {\n    /** Id or username */\n    user: string;\n}\n/**\n * Retrieve versions of the build that were built in the past and available for deployment,\n * sorted by most recent first.\n * Response: [ListBuildVersionsResponse].\n */\nexport interface ListBuildVersions {\n    /** Id or name */\n    build: string;\n    /** Filter to only include versions matching this major version. */\n    major?: number;\n    /** Filter to only include versions matching this minor version. */\n    minor?: number;\n    /** Filter to only include versions matching this patch version. */\n    patch?: number;\n    /** Limit the number of included results. Default is no limit. */\n    limit?: I64;\n}\n/** List builders matching structured query. Response: [ListBuildersResponse]. */\nexport interface ListBuilders {\n    query?: BuilderQuery;\n}\n/** List builds matching optional query. Response: [ListBuildsResponse]. */\nexport interface ListBuilds {\n    /** optional structured query to filter builds. */\n    query?: BuildQuery;\n}\n/**\n * Gets a list of existing values used as extra args across other builds.\n * Useful to offer suggestions. Response: [ListCommonBuildExtraArgsResponse]\n */\nexport interface ListCommonBuildExtraArgs {\n    /** optional structured query to filter builds. */\n    query?: BuildQuery;\n}\n/**\n * Gets a list of existing values used as extra args across other deployments.\n * Useful to offer suggestions. Response: [ListCommonDeploymentExtraArgsResponse]\n */\nexport interface ListCommonDeploymentExtraArgs {\n    /** optional structured query to filter deployments. */\n    query?: DeploymentQuery;\n}\n/**\n * Gets a list of existing values used as build extra args across other stacks.\n * Useful to offer suggestions. Response: [ListCommonStackBuildExtraArgsResponse]\n */\nexport interface ListCommonStackBuildExtraArgs {\n    /** optional structured query to filter stacks. */\n    query?: StackQuery;\n}\n/**\n * Gets a list of existing values used as extra args across other stacks.\n * Useful to offer suggestions. Response: [ListCommonStackExtraArgsResponse]\n */\nexport interface ListCommonStackExtraArgs {\n    /** optional structured query to filter stacks. */\n    query?: StackQuery;\n}\n/**\n * List all docker compose projects on the target server.\n * Response: [ListComposeProjectsResponse].\n */\nexport interface ListComposeProjects {\n    /** Id or name */\n    server: string;\n}\n/**\n * List deployments matching optional query.\n * Response: [ListDeploymentsResponse].\n */\nexport interface ListDeployments {\n    /** optional structured query to filter deployments. */\n    query?: DeploymentQuery;\n}\n/**\n * List all docker containers on the target server.\n * Response: [ListDockerContainersResponse].\n */\nexport interface ListDockerContainers {\n    /** Id or name */\n    server: string;\n}\n/** Get image history from the server. Response: [ListDockerImageHistoryResponse]. */\nexport interface ListDockerImageHistory {\n    /** Id or name */\n    server: string;\n    /** The image name */\n    image: string;\n}\n/**\n * List the docker images locally cached on the target server.\n * Response: [ListDockerImagesResponse].\n */\nexport interface ListDockerImages {\n    /** Id or name */\n    server: string;\n}\n/** List the docker networks on the server. Response: [ListDockerNetworksResponse]. */\nexport interface ListDockerNetworks {\n    /** Id or name */\n    server: string;\n}\n/**\n * List the docker registry providers available in Core / Periphery config files.\n * Response: [ListDockerRegistriesFromConfigResponse].\n *\n * Includes:\n * - registries in core config\n * - registries configured on builds, deployments\n * - registries on the optional Server or Builder\n */\nexport interface ListDockerRegistriesFromConfig {\n    /**\n     * Accepts an optional Server or Builder target to expand the core list with\n     * providers available on that specific resource.\n     */\n    target?: ResourceTarget;\n}\n/**\n * List docker registry accounts matching optional query.\n * Response: [ListDockerRegistryAccountsResponse].\n */\nexport interface ListDockerRegistryAccounts {\n    /** Optionally filter by accounts with a specific domain. */\n    domain?: string;\n    /** Optionally filter by accounts with a specific username. */\n    username?: string;\n}\n/**\n * List all docker volumes on the target server.\n * Response: [ListDockerVolumesResponse].\n */\nexport interface ListDockerVolumes {\n    /** Id or name */\n    server: string;\n}\n/** List actions matching optional query. Response: [ListFullActionsResponse]. */\nexport interface ListFullActions {\n    /** optional structured query to filter actions. */\n    query?: ActionQuery;\n}\n/** List full alerters matching optional query. Response: [ListFullAlertersResponse]. */\nexport interface ListFullAlerters {\n    /** Structured query to filter alerters. */\n    query?: AlerterQuery;\n}\n/** List builders matching structured query. Response: [ListFullBuildersResponse]. */\nexport interface ListFullBuilders {\n    query?: BuilderQuery;\n}\n/** List builds matching optional query. Response: [ListFullBuildsResponse]. */\nexport interface ListFullBuilds {\n    /** optional structured query to filter builds. */\n    query?: BuildQuery;\n}\n/**\n * List deployments matching optional query.\n * Response: [ListFullDeploymentsResponse].\n */\nexport interface ListFullDeployments {\n    /** optional structured query to filter deployments. */\n    query?: DeploymentQuery;\n}\n/** List procedures matching optional query. Response: [ListFullProceduresResponse]. */\nexport interface ListFullProcedures {\n    /** optional structured query to filter procedures. */\n    query?: ProcedureQuery;\n}\n/** List repos matching optional query. Response: [ListFullReposResponse]. */\nexport interface ListFullRepos {\n    /** optional structured query to filter repos. */\n    query?: RepoQuery;\n}\n/** List syncs matching optional query. Response: [ListFullResourceSyncsResponse]. */\nexport interface ListFullResourceSyncs {\n    /** optional structured query to filter syncs. */\n    query?: ResourceSyncQuery;\n}\n/** List servers matching optional query. Response: [ListFullServersResponse]. */\nexport interface ListFullServers {\n    /** optional structured query to filter servers. */\n    query?: ServerQuery;\n}\n/** List stacks matching optional query. Response: [ListFullStacksResponse]. */\nexport interface ListFullStacks {\n    /** optional structured query to filter stacks. */\n    query?: StackQuery;\n}\n/**\n * List git provider accounts matching optional query.\n * Response: [ListGitProviderAccountsResponse].\n */\nexport interface ListGitProviderAccounts {\n    /** Optionally filter by accounts with a specific domain. */\n    domain?: string;\n    /** Optionally filter by accounts with a specific username. */\n    username?: string;\n}\n/**\n * List the git providers available in Core / Periphery config files.\n * Response: [ListGitProvidersFromConfigResponse].\n *\n * Includes:\n * - providers in core config\n * - providers configured on builds, repos, syncs\n * - providers on the optional Server or Builder\n */\nexport interface ListGitProvidersFromConfig {\n    /**\n     * Accepts an optional Server or Builder target to expand the core list with\n     * providers available on that specific resource.\n     */\n    target?: ResourceTarget;\n}\n/**\n * List permissions for the calling user.\n * Does not include any permissions on UserGroups they may be a part of.\n * Response: [ListPermissionsResponse]\n */\nexport interface ListPermissions {\n}\n/** List procedures matching optional query. Response: [ListProceduresResponse]. */\nexport interface ListProcedures {\n    /** optional structured query to filter procedures. */\n    query?: ProcedureQuery;\n}\n/** List repos matching optional query. Response: [ListReposResponse]. */\nexport interface ListRepos {\n    /** optional structured query to filter repos. */\n    query?: RepoQuery;\n}\n/** List syncs matching optional query. Response: [ListResourceSyncsResponse]. */\nexport interface ListResourceSyncs {\n    /** optional structured query to filter syncs. */\n    query?: ResourceSyncQuery;\n}\n/**\n * List configured schedules.\n * Response: [ListSchedulesResponse].\n */\nexport interface ListSchedules {\n    /** Pass Vec of tag ids or tag names */\n    tags?: string[];\n    /** 'All' or 'Any' */\n    tag_behavior?: TagQueryBehavior;\n}\n/**\n * List the available secrets from the core config.\n * Response: [ListSecretsResponse].\n */\nexport interface ListSecrets {\n    /**\n     * Accepts an optional Server or Builder target to expand the core list with\n     * providers available on that specific resource.\n     */\n    target?: ResourceTarget;\n}\n/** List servers matching optional query. Response: [ListServersResponse]. */\nexport interface ListServers {\n    /** optional structured query to filter servers. */\n    query?: ServerQuery;\n}\n/** Lists a specific stacks services (the containers). Response: [ListStackServicesResponse]. */\nexport interface ListStackServices {\n    /** Id or name */\n    stack: string;\n}\n/** List stacks matching optional query. Response: [ListStacksResponse]. */\nexport interface ListStacks {\n    /** optional structured query to filter stacks. */\n    query?: StackQuery;\n}\n/**\n * List the processes running on the target server.\n * Response: [ListSystemProcessesResponse].\n *\n * Note. This does not hit the server directly. The procedures come from an\n * in memory cache on the core, which hits the server periodically\n * to keep it up to date.\n */\nexport interface ListSystemProcesses {\n    /** Id or name */\n    server: string;\n}\n/**\n * List data for tags matching optional mongo query.\n * Response: [ListTagsResponse].\n */\nexport interface ListTags {\n    query?: MongoDocument;\n}\n/**\n * List the current terminals on specified server.\n * Response: [ListTerminalsResponse].\n */\nexport interface ListTerminals {\n    /** Id or name */\n    server: string;\n    /**\n     * Force a fresh call to Periphery for the list.\n     * Otherwise the response will be cached for 30s\n     */\n    fresh?: boolean;\n}\n/**\n * Paginated endpoint for updates matching optional query.\n * More recent updates will be returned first.\n */\nexport interface ListUpdates {\n    /** An optional mongo query to filter the updates. */\n    query?: MongoDocument;\n    /**\n     * Page of updates. Default is 0, which is the most recent data.\n     * Use with the `next_page` field of the response.\n     */\n    page?: number;\n}\n/** Minimal representation of an action performed by Komodo. */\nexport interface UpdateListItem {\n    /** The id of the update */\n    id: string;\n    /** Which operation was run */\n    operation: Operation;\n    /** The starting time of the operation */\n    start_ts: I64;\n    /** Whether the operation was successful */\n    success: boolean;\n    /** The username of the user performing update */\n    username: string;\n    /**\n     * The user id that triggered the update.\n     *\n     * Also can take these values for operations triggered automatically:\n     * - `Procedure`: The operation was triggered as part of a procedure run\n     * - `Github`: The operation was triggered by a github webhook\n     * - `Auto Redeploy`: The operation (always `Deploy`) was triggered by an attached build finishing.\n     */\n    operator: string;\n    /** The target resource to which this update refers */\n    target: ResourceTarget;\n    /**\n     * The status of the update\n     * - `Queued`\n     * - `InProgress`\n     * - `Complete`\n     */\n    status: UpdateStatus;\n    /** An optional version on the update, ie build version or deployed version. */\n    version?: Version;\n    /** Some unstructured, operation specific data. Not for general usage. */\n    other_data?: string;\n}\n/** Response for [ListUpdates]. */\nexport interface ListUpdatesResponse {\n    /** The page of updates, sorted by timestamp descending. */\n    updates: UpdateListItem[];\n    /** If there is a next page of data, pass this to `page` to get it. */\n    next_page?: number;\n}\n/**\n * List all user groups which user can see. Response: [ListUserGroupsResponse].\n *\n * Admins can see all user groups,\n * and users can see user groups to which they belong.\n */\nexport interface ListUserGroups {\n}\n/**\n * List permissions for a specific user. **Admin only**.\n * Response: [ListUserTargetPermissionsResponse]\n */\nexport interface ListUserTargetPermissions {\n    /** Specify either a user or a user group. */\n    user_target: UserTarget;\n}\n/**\n * **Admin only.**\n * Gets list of Komodo users.\n * Response: [ListUsersResponse]\n */\nexport interface ListUsers {\n}\n/**\n * List all available global variables.\n * Response: [ListVariablesResponse]\n *\n * Note. For non admin users making this call,\n * secret variables will have their values obscured.\n */\nexport interface ListVariables {\n}\n/**\n * Login as a local user. Will fail if the users credentials don't match\n * any local user.\n *\n * Note. This method is only available if the core api has `local_auth` enabled.\n */\nexport interface LoginLocalUser {\n    /** The user's username */\n    username: string;\n    /** The user's password */\n    password: string;\n}\nexport interface NameAndId {\n    name: string;\n    id: string;\n}\n/** Configuration for a Ntfy alerter. */\nexport interface NtfyAlerterEndpoint {\n    /** The ntfy topic URL */\n    url: string;\n    /**\n     * Optional E-Mail Address to enable ntfy email notifications.\n     * SMTP must be configured on the ntfy server.\n     */\n    email?: string;\n}\n/** Pauses all containers on the target server. Response: [Update] */\nexport interface PauseAllContainers {\n    /** Name or id */\n    server: string;\n}\n/**\n * Pauses the container on the target server. Response: [Update]\n *\n * 1. Runs `docker pause ${container_name}`.\n */\nexport interface PauseContainer {\n    /** Name or id */\n    server: string;\n    /** The container name */\n    container: string;\n}\n/**\n * Pauses the container for the target deployment. Response: [Update]\n *\n * 1. Runs `docker pause ${container_name}`.\n */\nexport interface PauseDeployment {\n    /** Name or id */\n    deployment: string;\n}\n/** Pauses the target stack. `docker compose pause`. Response: [Update] */\nexport interface PauseStack {\n    /** Id or name */\n    stack: string;\n    /**\n     * Filter to only pause specific services.\n     * If empty, will pause all services.\n     */\n    services?: string[];\n}\nexport interface PermissionToml {\n    /**\n     * Id can be:\n     * - resource name. `id = \"abcd-build\"`\n     * - regex matching resource names. `id = \"\\^(.+)-build-([0-9]+)$\\\"`\n     */\n    target: ResourceTarget;\n    /**\n     * The permission level:\n     * - None\n     * - Read\n     * - Execute\n     * - Write\n     */\n    level?: PermissionLevel;\n    /** Any [SpecificPermissions](SpecificPermission) on the resource */\n    specific?: Array<SpecificPermission>;\n}\n/**\n * Prunes the docker buildx cache on the target server. Response: [Update].\n *\n * 1. Runs `docker buildx prune -a -f`.\n */\nexport interface PruneBuildx {\n    /** Id or name */\n    server: string;\n}\n/**\n * Prunes the docker containers on the target server. Response: [Update].\n *\n * 1. Runs `docker container prune -f`.\n */\nexport interface PruneContainers {\n    /** Id or name */\n    server: string;\n}\n/**\n * Prunes the docker builders (build cache) on the target server. Response: [Update].\n *\n * 1. Runs `docker builder prune -a -f`.\n */\nexport interface PruneDockerBuilders {\n    /** Id or name */\n    server: string;\n}\n/**\n * Prunes the docker images on the target server. Response: [Update].\n *\n * 1. Runs `docker image prune -a -f`.\n */\nexport interface PruneImages {\n    /** Id or name */\n    server: string;\n}\n/**\n * Prunes the docker networks on the target server. Response: [Update].\n *\n * 1. Runs `docker network prune -f`.\n */\nexport interface PruneNetworks {\n    /** Id or name */\n    server: string;\n}\n/**\n * Prunes the docker system on the target server, including volumes. Response: [Update].\n *\n * 1. Runs `docker system prune -a -f --volumes`.\n */\nexport interface PruneSystem {\n    /** Id or name */\n    server: string;\n}\n/**\n * Prunes the docker volumes on the target server. Response: [Update].\n *\n * 1. Runs `docker volume prune -a -f`.\n */\nexport interface PruneVolumes {\n    /** Id or name */\n    server: string;\n}\n/** Pulls the image for the target deployment. Response: [Update] */\nexport interface PullDeployment {\n    /** Name or id */\n    deployment: string;\n}\n/**\n * Pulls the target repo. Response: [Update].\n *\n * Note. Repo must have server attached at `server_id`.\n *\n * 1. Pulls the repo on the target server using `git pull`.\n * 2. If `on_pull` is specified, it will be executed after the pull is complete.\n */\nexport interface PullRepo {\n    /** Id or name */\n    repo: string;\n}\n/** Pulls images for the target stack. `docker compose pull`. Response: [Update] */\nexport interface PullStack {\n    /** Id or name */\n    stack: string;\n    /**\n     * Filter to only pull specific services.\n     * If empty, will pull all services.\n     */\n    services?: string[];\n}\n/**\n * Push a resource to the front of the users 10 most recently viewed resources.\n * Response: [NoData].\n */\nexport interface PushRecentlyViewed {\n    /** The target to push. */\n    resource: ResourceTarget;\n}\n/** Configuration for a Pushover alerter. */\nexport interface PushoverAlerterEndpoint {\n    /** The pushover URL including application and user tokens in parameters. */\n    url: string;\n}\n/** Trigger a refresh of the cached latest hash and message. */\nexport interface RefreshBuildCache {\n    /** Id or name */\n    build: string;\n}\n/** Trigger a refresh of the cached latest hash and message. */\nexport interface RefreshRepoCache {\n    /** Id or name */\n    repo: string;\n}\n/** Trigger a refresh of the computed diff logs for view. Response: [ResourceSync] */\nexport interface RefreshResourceSyncPending {\n    /** Id or name */\n    sync: string;\n}\n/**\n * Trigger a refresh of the cached compose file contents.\n * Refreshes:\n * - Whether the remote file is missing\n * - The latest json, and for repos, the remote contents, hash, and message.\n */\nexport interface RefreshStackCache {\n    /** Id or name */\n    stack: string;\n}\n/** **Admin only.** Remove a user from a user group. Response: [UserGroup] */\nexport interface RemoveUserFromUserGroup {\n    /** The name or id of UserGroup that user should be removed from. */\n    user_group: string;\n    /** The id or username of the user to remove */\n    user: string;\n}\n/**\n * Rename the Action at id to the given name.\n * Response: [Update].\n */\nexport interface RenameAction {\n    /** The id or name of the Action to rename. */\n    id: string;\n    /** The new name. */\n    name: string;\n}\n/**\n * Rename the Alerter at id to the given name.\n * Response: [Update].\n */\nexport interface RenameAlerter {\n    /** The id or name of the Alerter to rename. */\n    id: string;\n    /** The new name. */\n    name: string;\n}\n/**\n * Rename the Build at id to the given name.\n * Response: [Update].\n */\nexport interface RenameBuild {\n    /** The id or name of the Build to rename. */\n    id: string;\n    /** The new name. */\n    name: string;\n}\n/**\n * Rename the Builder at id to the given name.\n * Response: [Update].\n */\nexport interface RenameBuilder {\n    /** The id or name of the Builder to rename. */\n    id: string;\n    /** The new name. */\n    name: string;\n}\n/**\n * Rename the deployment at id to the given name. Response: [Update].\n *\n * Note. If a container is created for the deployment, it will be renamed using\n * `docker rename ...`.\n */\nexport interface RenameDeployment {\n    /** The id of the deployment to rename. */\n    id: string;\n    /** The new name. */\n    name: string;\n}\n/**\n * Rename the Procedure at id to the given name.\n * Response: [Update].\n */\nexport interface RenameProcedure {\n    /** The id or name of the Procedure to rename. */\n    id: string;\n    /** The new name. */\n    name: string;\n}\n/**\n * Rename the Repo at id to the given name.\n * Response: [Update].\n */\nexport interface RenameRepo {\n    /** The id or name of the Repo to rename. */\n    id: string;\n    /** The new name. */\n    name: string;\n}\n/**\n * Rename the ResourceSync at id to the given name.\n * Response: [Update].\n */\nexport interface RenameResourceSync {\n    /** The id or name of the ResourceSync to rename. */\n    id: string;\n    /** The new name. */\n    name: string;\n}\n/**\n * Rename an Server to the given name.\n * Response: [Update].\n */\nexport interface RenameServer {\n    /** The id or name of the Server to rename. */\n    id: string;\n    /** The new name. */\n    name: string;\n}\n/** Rename the stack at id to the given name. Response: [Update]. */\nexport interface RenameStack {\n    /** The id of the stack to rename. */\n    id: string;\n    /** The new name. */\n    name: string;\n}\n/** Rename a tag at id. Response: [Tag]. */\nexport interface RenameTag {\n    /** The id of the tag to rename. */\n    id: string;\n    /** The new name of the tag. */\n    name: string;\n}\n/** **Admin only.** Rename a user group. Response: [UserGroup] */\nexport interface RenameUserGroup {\n    /** The id of the UserGroup */\n    id: string;\n    /** The new name for the UserGroup */\n    name: string;\n}\nexport declare enum DefaultRepoFolder {\n    /** /${root_directory}/stacks */\n    Stacks = \"Stacks\",\n    /** /${root_directory}/builds */\n    Builds = \"Builds\",\n    /** /${root_directory}/repos */\n    Repos = \"Repos\",\n    /**\n     * If the repo is only cloned\n     * in the core repo cache (resource sync),\n     * this isn't relevant.\n     */\n    NotApplicable = \"NotApplicable\"\n}\nexport interface RepoExecutionArgs {\n    /** Resource name (eg Build name, Repo name) */\n    name: string;\n    /** Git provider domain. Default: `github.com` */\n    provider: string;\n    /** Use https (vs http). */\n    https: boolean;\n    /** Configure the account used to access repo (if private) */\n    account?: string;\n    /**\n     * Full repo identifier. {namespace}/{repo_name}\n     * Its optional to force checking and produce error if not defined.\n     */\n    repo?: string;\n    /** Git Branch. Default: `main` */\n    branch: string;\n    /** Specific commit hash. Optional */\n    commit?: string;\n    /** The clone destination path */\n    destination?: string;\n    /**\n     * The default folder to use.\n     * Depends on the resource type.\n     */\n    default_folder: DefaultRepoFolder;\n}\nexport interface RepoExecutionResponse {\n    /** Response logs */\n    logs: Log[];\n    /** Absolute path to the repo root on the host. */\n    path: string;\n    /** Latest short commit hash, if it could be retrieved */\n    commit_hash?: string;\n    /** Latest commit message, if it could be retrieved */\n    commit_message?: string;\n}\nexport interface ResourceToml<PartialConfig> {\n    /** The resource name. Required */\n    name: string;\n    /** The resource description. Optional. */\n    description?: string;\n    /** Mark resource as a template */\n    template?: boolean;\n    /** Tag ids or names. Optional */\n    tags?: string[];\n    /**\n     * Optional. Only relevant for deployments / stacks.\n     *\n     * Will ensure deployment / stack is running with the latest configuration.\n     * Deploy actions to achieve this will be included in the sync.\n     * Default is false.\n     */\n    deploy?: boolean;\n    /**\n     * Optional. Only relevant for deployments / stacks using the 'deploy' sync feature.\n     *\n     * Specify other deployments / stacks by name as dependencies.\n     * The sync will ensure the deployment / stack will only be deployed 'after' its dependencies.\n     */\n    after?: string[];\n    /** Resource specific configuration. */\n    config?: PartialConfig;\n}\nexport interface UserGroupToml {\n    /** User group name */\n    name: string;\n    /** Whether all users will implicitly have the permissions in this group. */\n    everyone?: boolean;\n    /** Users in the group */\n    users?: string[];\n    /** Give the user group elevated permissions on all resources of a certain type */\n    all?: Record<ResourceTarget[\"type\"], PermissionLevelAndSpecifics | PermissionLevel>;\n    /** Permissions given to the group */\n    permissions?: PermissionToml[];\n}\n/** Specifies resources to sync on Komodo */\nexport interface ResourcesToml {\n    servers?: ResourceToml<_PartialServerConfig>[];\n    deployments?: ResourceToml<_PartialDeploymentConfig>[];\n    stacks?: ResourceToml<_PartialStackConfig>[];\n    builds?: ResourceToml<_PartialBuildConfig>[];\n    repos?: ResourceToml<_PartialRepoConfig>[];\n    procedures?: ResourceToml<_PartialProcedureConfig>[];\n    actions?: ResourceToml<_PartialActionConfig>[];\n    alerters?: ResourceToml<_PartialAlerterConfig>[];\n    builders?: ResourceToml<_PartialBuilderConfig>[];\n    resource_syncs?: ResourceToml<_PartialResourceSyncConfig>[];\n    user_groups?: UserGroupToml[];\n    variables?: Variable[];\n}\n/** Restarts all containers on the target server. Response: [Update] */\nexport interface RestartAllContainers {\n    /** Name or id */\n    server: string;\n}\n/**\n * Restarts the container on the target server. Response: [Update]\n *\n * 1. Runs `docker restart ${container_name}`.\n */\nexport interface RestartContainer {\n    /** Name or id */\n    server: string;\n    /** The container name */\n    container: string;\n}\n/**\n * Restarts the container for the target deployment. Response: [Update]\n *\n * 1. Runs `docker restart ${container_name}`.\n */\nexport interface RestartDeployment {\n    /** Name or id */\n    deployment: string;\n}\n/** Restarts the target stack. `docker compose restart`. Response: [Update] */\nexport interface RestartStack {\n    /** Id or name */\n    stack: string;\n    /**\n     * Filter to only restart specific services.\n     * If empty, will restart all services.\n     */\n    services?: string[];\n}\n/** Runs the target Action. Response: [Update] */\nexport interface RunAction {\n    /** Id or name */\n    action: string;\n    /**\n     * Custom arguments which are merged on top of the default arguments.\n     * CLI Format: `\"VAR1=val1&VAR2=val2\"`\n     *\n     * Webhook-triggered actions use this to pass WEBHOOK_BRANCH and WEBHOOK_BODY.\n     */\n    args?: JsonObject;\n}\n/**\n * Runs the target build. Response: [Update].\n *\n * 1. Get a handle to the builder. If using AWS builder, this means starting a builder ec2 instance.\n *\n * 2. Clone the repo on the builder. If an `on_clone` commmand is given, it will be executed.\n *\n * 3. Execute `docker build {...params}`, where params are determined using the builds configuration.\n *\n * 4. If a docker registry is configured, the build will be pushed to the registry.\n *\n * 5. If using AWS builder, destroy the builder ec2 instance.\n *\n * 6. Deploy any Deployments with *Redeploy on Build* enabled.\n */\nexport interface RunBuild {\n    /** Can be build id or name */\n    build: string;\n}\n/** Runs the target Procedure. Response: [Update] */\nexport interface RunProcedure {\n    /** Id or name */\n    procedure: string;\n}\n/** Runs a one-time command against a service using `docker compose run`. Response: [Update] */\nexport interface RunStackService {\n    /** Id or name */\n    stack: string;\n    /** Service to run */\n    service: string;\n    /** Command and args to pass to the service container */\n    command?: string[];\n    /** Do not allocate TTY */\n    no_tty?: boolean;\n    /** Do not start linked services */\n    no_deps?: boolean;\n    /** Detach container on run */\n    detach?: boolean;\n    /** Map service ports to the host */\n    service_ports?: boolean;\n    /** Extra environment variables for the run */\n    env?: Record<string, string>;\n    /** Working directory inside the container */\n    workdir?: string;\n    /** User to run as inside the container */\n    user?: string;\n    /** Override the default entrypoint */\n    entrypoint?: string;\n    /** Pull the image before running */\n    pull?: boolean;\n}\n/** Runs the target resource sync. Response: [Update] */\nexport interface RunSync {\n    /** Id or name */\n    sync: string;\n    /**\n     * Only execute sync on a specific resource type.\n     * Combine with `resource_id` to specify resource.\n     */\n    resource_type?: ResourceTarget[\"type\"];\n    /**\n     * Only execute sync on a specific resources.\n     * Combine with `resource_type` to specify resources.\n     * Supports name or id.\n     */\n    resources?: string[];\n}\nexport declare enum SearchCombinator {\n    Or = \"Or\",\n    And = \"And\"\n}\n/**\n * Search the container log's tail using `grep`. All lines go to stdout.\n * Response: [Log].\n *\n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface SearchContainerLog {\n    /** Id or name */\n    server: string;\n    /** The container name */\n    container: string;\n    /** The terms to search for. */\n    terms: string[];\n    /**\n     * When searching for multiple terms, can use `AND` or `OR` combinator.\n     *\n     * - `AND`: Only include lines with **all** terms present in that line.\n     * - `OR`: Include lines that have one or more matches in the terms.\n     */\n    combinator?: SearchCombinator;\n    /** Invert the results, ie return all lines that DON'T match the terms / combinator. */\n    invert?: boolean;\n    /** Enable `--timestamps` */\n    timestamps?: boolean;\n}\n/**\n * Search the deployment log's tail using `grep`. All lines go to stdout.\n * Response: [Log].\n *\n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface SearchDeploymentLog {\n    /** Id or name */\n    deployment: string;\n    /** The terms to search for. */\n    terms: string[];\n    /**\n     * When searching for multiple terms, can use `AND` or `OR` combinator.\n     *\n     * - `AND`: Only include lines with **all** terms present in that line.\n     * - `OR`: Include lines that have one or more matches in the terms.\n     */\n    combinator?: SearchCombinator;\n    /** Invert the results, ie return all lines that DON'T match the terms / combinator. */\n    invert?: boolean;\n    /** Enable `--timestamps` */\n    timestamps?: boolean;\n}\n/**\n * Search the stack log's tail using `grep`. All lines go to stdout.\n * Response: [SearchStackLogResponse].\n *\n * Note. This call will hit the underlying server directly for most up to date log.\n */\nexport interface SearchStackLog {\n    /** Id or name */\n    stack: string;\n    /**\n     * Filter the logs to only ones from specific services.\n     * If empty, will include logs from all services.\n     */\n    services: string[];\n    /** The terms to search for. */\n    terms: string[];\n    /**\n     * When searching for multiple terms, can use `AND` or `OR` combinator.\n     *\n     * - `AND`: Only include lines with **all** terms present in that line.\n     * - `OR`: Include lines that have one or more matches in the terms.\n     */\n    combinator?: SearchCombinator;\n    /** Invert the results, ie return all lines that DON'T match the terms / combinator. */\n    invert?: boolean;\n    /** Enable `--timestamps` */\n    timestamps?: boolean;\n}\n/** Send a custom alert message to configured Alerters. Response: [Update] */\nexport interface SendAlert {\n    /** The alert level. */\n    level?: SeverityLevel;\n    /** The alert message. Required. */\n    message: string;\n    /** The alert details. Optional. */\n    details?: string;\n    /**\n     * Specific alerter names or ids.\n     * If empty / not passed, sends to all configured alerters\n     * with the `Custom` alert type whitelisted / not blacklisted.\n     */\n    alerters?: string[];\n}\n/** Configuration for a Komodo Server Builder. */\nexport interface ServerBuilderConfig {\n    /** The server id of the builder */\n    server_id?: string;\n}\n/** The health of a part of the server. */\nexport interface ServerHealthState {\n    level: SeverityLevel;\n    /** Whether the health is good enough to close an open alert. */\n    should_close_alert: boolean;\n}\n/** Summary of the health of the server. */\nexport interface ServerHealth {\n    cpu: ServerHealthState;\n    mem: ServerHealthState;\n    disks: Record<string, ServerHealthState>;\n}\n/**\n * **Admin only.** Set `everyone` property of User Group.\n * Response: [UserGroup]\n */\nexport interface SetEveryoneUserGroup {\n    /** Id or name. */\n    user_group: string;\n    /** Whether this user group applies to everyone. */\n    everyone: boolean;\n}\n/**\n * Set the time the user last opened the UI updates.\n * Used for unseen notification dot.\n * Response: [NoData]\n */\nexport interface SetLastSeenUpdate {\n}\n/**\n * **Admin only.** Completely override the users in the group.\n * Response: [UserGroup]\n */\nexport interface SetUsersInUserGroup {\n    /** Id or name. */\n    user_group: string;\n    /** The user ids or usernames to hard set as the group's users. */\n    users: string[];\n}\n/**\n * Sign up a new local user account. Will fail if a user with the\n * given username already exists.\n * Response: [SignUpLocalUserResponse].\n *\n * Note. This method is only available if the core api has `local_auth` enabled,\n * and if user registration is not disabled (after the first user).\n */\nexport interface SignUpLocalUser {\n    /** The username for the new user. */\n    username: string;\n    /**\n     * The password for the new user.\n     * This cannot be retreived later.\n     */\n    password: string;\n}\n/** Info for network interface usage. */\nexport interface SingleNetworkInterfaceUsage {\n    /** The network interface name */\n    name: string;\n    /** The ingress in bytes */\n    ingress_bytes: number;\n    /** The egress in bytes */\n    egress_bytes: number;\n}\n/** Configuration for a Slack alerter. */\nexport interface SlackAlerterEndpoint {\n    /** The Slack app webhook url */\n    url: string;\n}\n/** Sleeps for the specified time. */\nexport interface Sleep {\n    duration_ms?: I64;\n}\n/** Starts all containers on the target server. Response: [Update] */\nexport interface StartAllContainers {\n    /** Name or id */\n    server: string;\n}\n/**\n * Starts the container on the target server. Response: [Update]\n *\n * 1. Runs `docker start ${container_name}`.\n */\nexport interface StartContainer {\n    /** Name or id */\n    server: string;\n    /** The container name */\n    container: string;\n}\n/**\n * Starts the container for the target deployment. Response: [Update]\n *\n * 1. Runs `docker start ${container_name}`.\n */\nexport interface StartDeployment {\n    /** Name or id */\n    deployment: string;\n}\n/** Starts the target stack. `docker compose start`. Response: [Update] */\nexport interface StartStack {\n    /** Id or name */\n    stack: string;\n    /**\n     * Filter to only start specific services.\n     * If empty, will start all services.\n     */\n    services?: string[];\n}\n/** Stops all containers on the target server. Response: [Update] */\nexport interface StopAllContainers {\n    /** Name or id */\n    server: string;\n}\n/**\n * Stops the container on the target server. Response: [Update]\n *\n * 1. Runs `docker stop ${container_name}`.\n */\nexport interface StopContainer {\n    /** Name or id */\n    server: string;\n    /** The container name */\n    container: string;\n    /** Override the default termination signal. */\n    signal?: TerminationSignal;\n    /** Override the default termination max time. */\n    time?: number;\n}\n/**\n * Stops the container for the target deployment. Response: [Update]\n *\n * 1. Runs `docker stop ${container_name}`.\n */\nexport interface StopDeployment {\n    /** Name or id */\n    deployment: string;\n    /** Override the default termination signal specified in the deployment. */\n    signal?: TerminationSignal;\n    /** Override the default termination max time. */\n    time?: number;\n}\n/** Stops the target stack. `docker compose stop`. Response: [Update] */\nexport interface StopStack {\n    /** Id or name */\n    stack: string;\n    /** Override the default termination max time. */\n    stop_time?: number;\n    /**\n     * Filter to only stop specific services.\n     * If empty, will stop all services.\n     */\n    services?: string[];\n}\nexport interface TerminationSignalLabel {\n    signal: TerminationSignal;\n    label: string;\n}\n/** Tests an Alerters ability to reach the configured endpoint. Response: [Update] */\nexport interface TestAlerter {\n    /** Name or id */\n    alerter: string;\n}\n/** Info for the all system disks combined. */\nexport interface TotalDiskUsage {\n    /** Used portion in GB */\n    used_gb: number;\n    /** Total size in GB */\n    total_gb: number;\n}\n/** Unpauses all containers on the target server. Response: [Update] */\nexport interface UnpauseAllContainers {\n    /** Name or id */\n    server: string;\n}\n/**\n * Unpauses the container on the target server. Response: [Update]\n *\n * 1. Runs `docker unpause ${container_name}`.\n *\n * Note. This is the only way to restart a paused container.\n */\nexport interface UnpauseContainer {\n    /** Name or id */\n    server: string;\n    /** The container name */\n    container: string;\n}\n/**\n * Unpauses the container for the target deployment. Response: [Update]\n *\n * 1. Runs `docker unpause ${container_name}`.\n *\n * Note. This is the only way to restart a paused container.\n */\nexport interface UnpauseDeployment {\n    /** Name or id */\n    deployment: string;\n}\n/**\n * Unpauses the target stack. `docker compose unpause`. Response: [Update].\n *\n * Note. This is the only way to restart a paused container.\n */\nexport interface UnpauseStack {\n    /** Id or name */\n    stack: string;\n    /**\n     * Filter to only unpause specific services.\n     * If empty, will unpause all services.\n     */\n    services?: string[];\n}\n/**\n * Update the action at the given id, and return the updated action.\n * Response: [Action].\n *\n * Note. This method updates only the fields which are set in the [_PartialActionConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateAction {\n    /** The id of the action to update. */\n    id: string;\n    /** The partial config update to apply. */\n    config: _PartialActionConfig;\n}\n/**\n * Update the alerter at the given id, and return the updated alerter. Response: [Alerter].\n *\n * Note. This method updates only the fields which are set in the [PartialAlerterConfig][crate::entities::alerter::PartialAlerterConfig],\n * effectively merging diffs into the final document. This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateAlerter {\n    /** The id of the alerter to update. */\n    id: string;\n    /** The partial config update to apply. */\n    config: _PartialAlerterConfig;\n}\n/**\n * Update the build at the given id, and return the updated build.\n * Response: [Build].\n *\n * Note. This method updates only the fields which are set in the [_PartialBuildConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateBuild {\n    /** The id or name of the build to update. */\n    id: string;\n    /** The partial config update to apply. */\n    config: _PartialBuildConfig;\n}\n/**\n * Update the builder at the given id, and return the updated builder.\n * Response: [Builder].\n *\n * Note. This method updates only the fields which are set in the [PartialBuilderConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateBuilder {\n    /** The id of the builder to update. */\n    id: string;\n    /** The partial config update to apply. */\n    config: PartialBuilderConfig;\n}\n/**\n * Update the deployment at the given id, and return the updated deployment.\n * Response: [Deployment].\n *\n * Note. If the attached server for the deployment changes,\n * the deployment will be deleted / cleaned up on the old server.\n *\n * Note. This method updates only the fields which are set in the [_PartialDeploymentConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateDeployment {\n    /** The deployment id to update. */\n    id: string;\n    /** The partial config update. */\n    config: _PartialDeploymentConfig;\n}\n/**\n * **Admin only.** Update a docker registry account.\n * Response: [DockerRegistryAccount].\n */\nexport interface UpdateDockerRegistryAccount {\n    /** The id of the docker registry to update */\n    id: string;\n    /** The partial docker registry account. */\n    account: _PartialDockerRegistryAccount;\n}\n/**\n * **Admin only.** Update a git provider account.\n * Response: [GitProviderAccount].\n */\nexport interface UpdateGitProviderAccount {\n    /** The id of the git provider account to update. */\n    id: string;\n    /** The partial git provider account. */\n    account: _PartialGitProviderAccount;\n}\n/**\n * **Admin only.** Update a user or user groups base permission level on a resource type.\n * Response: [NoData].\n */\nexport interface UpdatePermissionOnResourceType {\n    /** Specify the user or user group. */\n    user_target: UserTarget;\n    /** The resource type: eg. Server, Build, Deployment, etc. */\n    resource_type: ResourceTarget[\"type\"];\n    /** The base permission level. */\n    permission: PermissionLevelAndSpecifics | PermissionLevel;\n}\n/**\n * **Admin only.** Update a user or user groups permission on a resource.\n * Response: [NoData].\n */\nexport interface UpdatePermissionOnTarget {\n    /** Specify the user or user group. */\n    user_target: UserTarget;\n    /** Specify the target resource. */\n    resource_target: ResourceTarget;\n    /** Specify the permission level. */\n    permission: PermissionLevelAndSpecifics | PermissionLevel;\n}\n/**\n * Update the procedure at the given id, and return the updated procedure.\n * Response: [Procedure].\n *\n * Note. This method updates only the fields which are set in the [_PartialProcedureConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateProcedure {\n    /** The id of the procedure to update. */\n    id: string;\n    /** The partial config update. */\n    config: _PartialProcedureConfig;\n}\n/**\n * Update the repo at the given id, and return the updated repo.\n * Response: [Repo].\n *\n * Note. If the attached server for the repo changes,\n * the repo will be deleted / cleaned up on the old server.\n *\n * Note. This method updates only the fields which are set in the [_PartialRepoConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateRepo {\n    /** The id of the repo to update. */\n    id: string;\n    /** The partial config update to apply. */\n    config: _PartialRepoConfig;\n}\n/**\n * Update a resources common meta fields.\n * - description\n * - template\n * - tags\n * Response: [NoData].\n */\nexport interface UpdateResourceMeta {\n    /** The target resource to set update meta. */\n    target: ResourceTarget;\n    /**\n     * New description to set,\n     * or null for no update\n     */\n    description?: string;\n    /**\n     * New template value (true or false),\n     * or null for no update\n     */\n    template?: boolean;\n    /**\n     * The exact tags to set,\n     * or null for no update\n     */\n    tags?: string[];\n}\n/**\n * Update the sync at the given id, and return the updated sync.\n * Response: [ResourceSync].\n *\n * Note. This method updates only the fields which are set in the [_PartialResourceSyncConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateResourceSync {\n    /** The id of the sync to update. */\n    id: string;\n    /** The partial config update to apply. */\n    config: _PartialResourceSyncConfig;\n}\n/**\n * Update the server at the given id, and return the updated server.\n * Response: [Server].\n *\n * Note. This method updates only the fields which are set in the [_PartialServerConfig],\n * effectively merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateServer {\n    /** The id or name of the server to update. */\n    id: string;\n    /** The partial config update to apply. */\n    config: _PartialServerConfig;\n}\n/**\n * **Admin only.** Update a service user's description.\n * Response: [User].\n */\nexport interface UpdateServiceUserDescription {\n    /** The service user's username */\n    username: string;\n    /** A new description for the service user. */\n    description: string;\n}\n/**\n * Update the stack at the given id, and return the updated stack.\n * Response: [Stack].\n *\n * Note. If the attached server for the stack changes,\n * the stack will be deleted / cleaned up on the old server.\n *\n * Note. This method updates only the fields which are set in the [_PartialStackConfig],\n * merging diffs into the final document.\n * This is helpful when multiple users are using\n * the same resources concurrently by ensuring no unintentional\n * field changes occur from out of date local state.\n */\nexport interface UpdateStack {\n    /** The id of the Stack to update. */\n    id: string;\n    /** The partial config update to apply. */\n    config: _PartialStackConfig;\n}\n/** Update color for tag. Response: [Tag]. */\nexport interface UpdateTagColor {\n    /** The name or id of the tag to update. */\n    tag: string;\n    /** The new color for the tag. */\n    color: TagColor;\n}\n/**\n * **Super Admin only.** Update's whether a user is admin.\n * Response: [NoData].\n */\nexport interface UpdateUserAdmin {\n    /** The target user. */\n    user_id: string;\n    /** Whether user should be admin. */\n    admin: boolean;\n}\n/**\n * **Admin only.** Update a user's \"base\" permissions, eg. \"enabled\".\n * Response: [NoData].\n */\nexport interface UpdateUserBasePermissions {\n    /** The target user. */\n    user_id: string;\n    /** If specified, will update users enabled state. */\n    enabled?: boolean;\n    /** If specified, will update user's ability to create servers. */\n    create_servers?: boolean;\n    /** If specified, will update user's ability to create builds. */\n    create_builds?: boolean;\n}\n/**\n * **Only for local users**. Update the calling users password.\n * Response: [NoData].\n */\nexport interface UpdateUserPassword {\n    password: string;\n}\n/**\n * **Only for local users**. Update the calling users username.\n * Response: [NoData].\n */\nexport interface UpdateUserUsername {\n    username: string;\n}\n/** **Admin only.** Update variable description. Response: [Variable]. */\nexport interface UpdateVariableDescription {\n    /** The name of the variable to update. */\n    name: string;\n    /** The description to set. */\n    description: string;\n}\n/** **Admin only.** Update whether variable is secret. Response: [Variable]. */\nexport interface UpdateVariableIsSecret {\n    /** The name of the variable to update. */\n    name: string;\n    /** Whether variable is secret. */\n    is_secret: boolean;\n}\n/** **Admin only.** Update variable value. Response: [Variable]. */\nexport interface UpdateVariableValue {\n    /** The name of the variable to update. */\n    name: string;\n    /** The value to set. */\n    value: string;\n}\n/** Configuration for a Komodo Url Builder. */\nexport interface UrlBuilderConfig {\n    /** The address of the Periphery agent */\n    address: string;\n    /** A custom passkey to use. Otherwise, use the default passkey. */\n    passkey?: string;\n}\n/** Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update]. */\nexport interface WriteBuildFileContents {\n    /** The name or id of the target Build. */\n    build: string;\n    /** The dockerfile contents to write. */\n    contents: string;\n}\n/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */\nexport interface WriteStackFileContents {\n    /** The name or id of the target Stack. */\n    stack: string;\n    /**\n     * The file path relative to the stack run directory,\n     * or absolute path.\n     */\n    file_path: string;\n    /** The contents to write. */\n    contents: string;\n}\n/** Rename the stack at id to the given name. Response: [Update]. */\nexport interface WriteSyncFileContents {\n    /** The name or id of the target Sync. */\n    sync: string;\n    /**\n     * If this file was under a resource folder, this will be the folder.\n     * Otherwise, it should be empty string.\n     */\n    resource_path: string;\n    /** The file path relative to the resource path. */\n    file_path: string;\n    /** The contents to write. */\n    contents: string;\n}\nexport type AuthRequest = {\n    type: \"GetLoginOptions\";\n    params: GetLoginOptions;\n} | {\n    type: \"SignUpLocalUser\";\n    params: SignUpLocalUser;\n} | {\n    type: \"LoginLocalUser\";\n    params: LoginLocalUser;\n} | {\n    type: \"ExchangeForJwt\";\n    params: ExchangeForJwt;\n} | {\n    type: \"GetUser\";\n    params: GetUser;\n};\n/** Days of the week */\nexport declare enum DayOfWeek {\n    Monday = \"Monday\",\n    Tuesday = \"Tuesday\",\n    Wednesday = \"Wednesday\",\n    Thursday = \"Thursday\",\n    Friday = \"Friday\",\n    Saturday = \"Saturday\",\n    Sunday = \"Sunday\"\n}\nexport type ExecuteRequest = {\n    type: \"StartContainer\";\n    params: StartContainer;\n} | {\n    type: \"RestartContainer\";\n    params: RestartContainer;\n} | {\n    type: \"PauseContainer\";\n    params: PauseContainer;\n} | {\n    type: \"UnpauseContainer\";\n    params: UnpauseContainer;\n} | {\n    type: \"StopContainer\";\n    params: StopContainer;\n} | {\n    type: \"DestroyContainer\";\n    params: DestroyContainer;\n} | {\n    type: \"StartAllContainers\";\n    params: StartAllContainers;\n} | {\n    type: \"RestartAllContainers\";\n    params: RestartAllContainers;\n} | {\n    type: \"PauseAllContainers\";\n    params: PauseAllContainers;\n} | {\n    type: \"UnpauseAllContainers\";\n    params: UnpauseAllContainers;\n} | {\n    type: \"StopAllContainers\";\n    params: StopAllContainers;\n} | {\n    type: \"PruneContainers\";\n    params: PruneContainers;\n} | {\n    type: \"DeleteNetwork\";\n    params: DeleteNetwork;\n} | {\n    type: \"PruneNetworks\";\n    params: PruneNetworks;\n} | {\n    type: \"DeleteImage\";\n    params: DeleteImage;\n} | {\n    type: \"PruneImages\";\n    params: PruneImages;\n} | {\n    type: \"DeleteVolume\";\n    params: DeleteVolume;\n} | {\n    type: \"PruneVolumes\";\n    params: PruneVolumes;\n} | {\n    type: \"PruneDockerBuilders\";\n    params: PruneDockerBuilders;\n} | {\n    type: \"PruneBuildx\";\n    params: PruneBuildx;\n} | {\n    type: \"PruneSystem\";\n    params: PruneSystem;\n} | {\n    type: \"DeployStack\";\n    params: DeployStack;\n} | {\n    type: \"BatchDeployStack\";\n    params: BatchDeployStack;\n} | {\n    type: \"DeployStackIfChanged\";\n    params: DeployStackIfChanged;\n} | {\n    type: \"BatchDeployStackIfChanged\";\n    params: BatchDeployStackIfChanged;\n} | {\n    type: \"PullStack\";\n    params: PullStack;\n} | {\n    type: \"BatchPullStack\";\n    params: BatchPullStack;\n} | {\n    type: \"StartStack\";\n    params: StartStack;\n} | {\n    type: \"RestartStack\";\n    params: RestartStack;\n} | {\n    type: \"StopStack\";\n    params: StopStack;\n} | {\n    type: \"PauseStack\";\n    params: PauseStack;\n} | {\n    type: \"UnpauseStack\";\n    params: UnpauseStack;\n} | {\n    type: \"DestroyStack\";\n    params: DestroyStack;\n} | {\n    type: \"BatchDestroyStack\";\n    params: BatchDestroyStack;\n} | {\n    type: \"RunStackService\";\n    params: RunStackService;\n} | {\n    type: \"Deploy\";\n    params: Deploy;\n} | {\n    type: \"BatchDeploy\";\n    params: BatchDeploy;\n} | {\n    type: \"PullDeployment\";\n    params: PullDeployment;\n} | {\n    type: \"StartDeployment\";\n    params: StartDeployment;\n} | {\n    type: \"RestartDeployment\";\n    params: RestartDeployment;\n} | {\n    type: \"PauseDeployment\";\n    params: PauseDeployment;\n} | {\n    type: \"UnpauseDeployment\";\n    params: UnpauseDeployment;\n} | {\n    type: \"StopDeployment\";\n    params: StopDeployment;\n} | {\n    type: \"DestroyDeployment\";\n    params: DestroyDeployment;\n} | {\n    type: \"BatchDestroyDeployment\";\n    params: BatchDestroyDeployment;\n} | {\n    type: \"RunBuild\";\n    params: RunBuild;\n} | {\n    type: \"BatchRunBuild\";\n    params: BatchRunBuild;\n} | {\n    type: \"CancelBuild\";\n    params: CancelBuild;\n} | {\n    type: \"CloneRepo\";\n    params: CloneRepo;\n} | {\n    type: \"BatchCloneRepo\";\n    params: BatchCloneRepo;\n} | {\n    type: \"PullRepo\";\n    params: PullRepo;\n} | {\n    type: \"BatchPullRepo\";\n    params: BatchPullRepo;\n} | {\n    type: \"BuildRepo\";\n    params: BuildRepo;\n} | {\n    type: \"BatchBuildRepo\";\n    params: BatchBuildRepo;\n} | {\n    type: \"CancelRepoBuild\";\n    params: CancelRepoBuild;\n} | {\n    type: \"RunProcedure\";\n    params: RunProcedure;\n} | {\n    type: \"BatchRunProcedure\";\n    params: BatchRunProcedure;\n} | {\n    type: \"RunAction\";\n    params: RunAction;\n} | {\n    type: \"BatchRunAction\";\n    params: BatchRunAction;\n} | {\n    type: \"TestAlerter\";\n    params: TestAlerter;\n} | {\n    type: \"SendAlert\";\n    params: SendAlert;\n} | {\n    type: \"RunSync\";\n    params: RunSync;\n} | {\n    type: \"ClearRepoCache\";\n    params: ClearRepoCache;\n} | {\n    type: \"BackupCoreDatabase\";\n    params: BackupCoreDatabase;\n} | {\n    type: \"GlobalAutoUpdate\";\n    params: GlobalAutoUpdate;\n};\n/**\n * One representative IANA zone for each distinct base UTC offset in the tz database.\n * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n *\n * The `serde`/`strum` renames ensure the canonical identifier is used\n * when serializing or parsing from a string such as `\"Etc/UTC\"`.\n */\nexport declare enum IanaTimezone {\n    /** UTC−12:00 */\n    EtcGmtMinus12 = \"Etc/GMT+12\",\n    /** UTC−11:00 */\n    PacificPagoPago = \"Pacific/Pago_Pago\",\n    /** UTC−10:00 */\n    PacificHonolulu = \"Pacific/Honolulu\",\n    /** UTC−09:30 */\n    PacificMarquesas = \"Pacific/Marquesas\",\n    /** UTC−09:00 */\n    AmericaAnchorage = \"America/Anchorage\",\n    /** UTC−08:00 */\n    AmericaLosAngeles = \"America/Los_Angeles\",\n    /** UTC−07:00 */\n    AmericaDenver = \"America/Denver\",\n    /** UTC−06:00 */\n    AmericaChicago = \"America/Chicago\",\n    /** UTC−05:00 */\n    AmericaNewYork = \"America/New_York\",\n    /** UTC−04:00 */\n    AmericaHalifax = \"America/Halifax\",\n    /** UTC−03:30 */\n    AmericaStJohns = \"America/St_Johns\",\n    /** UTC−03:00 */\n    AmericaSaoPaulo = \"America/Sao_Paulo\",\n    /** UTC−02:00 */\n    AmericaNoronha = \"America/Noronha\",\n    /** UTC−01:00 */\n    AtlanticAzores = \"Atlantic/Azores\",\n    /** UTC±00:00 */\n    EtcUtc = \"Etc/UTC\",\n    /** UTC+01:00 */\n    EuropeBerlin = \"Europe/Berlin\",\n    /** UTC+02:00 */\n    EuropeBucharest = \"Europe/Bucharest\",\n    /** UTC+03:00 */\n    EuropeMoscow = \"Europe/Moscow\",\n    /** UTC+03:30 */\n    AsiaTehran = \"Asia/Tehran\",\n    /** UTC+04:00 */\n    AsiaDubai = \"Asia/Dubai\",\n    /** UTC+04:30 */\n    AsiaKabul = \"Asia/Kabul\",\n    /** UTC+05:00 */\n    AsiaKarachi = \"Asia/Karachi\",\n    /** UTC+05:30 */\n    AsiaKolkata = \"Asia/Kolkata\",\n    /** UTC+05:45 */\n    AsiaKathmandu = \"Asia/Kathmandu\",\n    /** UTC+06:00 */\n    AsiaDhaka = \"Asia/Dhaka\",\n    /** UTC+06:30 */\n    AsiaYangon = \"Asia/Yangon\",\n    /** UTC+07:00 */\n    AsiaBangkok = \"Asia/Bangkok\",\n    /** UTC+08:00 */\n    AsiaShanghai = \"Asia/Shanghai\",\n    /** UTC+08:45 */\n    AustraliaEucla = \"Australia/Eucla\",\n    /** UTC+09:00 */\n    AsiaTokyo = \"Asia/Tokyo\",\n    /** UTC+09:30 */\n    AustraliaAdelaide = \"Australia/Adelaide\",\n    /** UTC+10:00 */\n    AustraliaSydney = \"Australia/Sydney\",\n    /** UTC+10:30 */\n    AustraliaLordHowe = \"Australia/Lord_Howe\",\n    /** UTC+11:00 */\n    PacificPortMoresby = \"Pacific/Port_Moresby\",\n    /** UTC+12:00 */\n    PacificAuckland = \"Pacific/Auckland\",\n    /** UTC+12:45 */\n    PacificChatham = \"Pacific/Chatham\",\n    /** UTC+13:00 */\n    PacificTongatapu = \"Pacific/Tongatapu\",\n    /** UTC+14:00 */\n    PacificKiritimati = \"Pacific/Kiritimati\"\n}\nexport type ReadRequest = {\n    type: \"GetVersion\";\n    params: GetVersion;\n} | {\n    type: \"GetCoreInfo\";\n    params: GetCoreInfo;\n} | {\n    type: \"ListSecrets\";\n    params: ListSecrets;\n} | {\n    type: \"ListGitProvidersFromConfig\";\n    params: ListGitProvidersFromConfig;\n} | {\n    type: \"ListDockerRegistriesFromConfig\";\n    params: ListDockerRegistriesFromConfig;\n} | {\n    type: \"GetUsername\";\n    params: GetUsername;\n} | {\n    type: \"GetPermission\";\n    params: GetPermission;\n} | {\n    type: \"FindUser\";\n    params: FindUser;\n} | {\n    type: \"ListUsers\";\n    params: ListUsers;\n} | {\n    type: \"ListApiKeys\";\n    params: ListApiKeys;\n} | {\n    type: \"ListApiKeysForServiceUser\";\n    params: ListApiKeysForServiceUser;\n} | {\n    type: \"ListPermissions\";\n    params: ListPermissions;\n} | {\n    type: \"ListUserTargetPermissions\";\n    params: ListUserTargetPermissions;\n} | {\n    type: \"GetUserGroup\";\n    params: GetUserGroup;\n} | {\n    type: \"ListUserGroups\";\n    params: ListUserGroups;\n} | {\n    type: \"GetProceduresSummary\";\n    params: GetProceduresSummary;\n} | {\n    type: \"GetProcedure\";\n    params: GetProcedure;\n} | {\n    type: \"GetProcedureActionState\";\n    params: GetProcedureActionState;\n} | {\n    type: \"ListProcedures\";\n    params: ListProcedures;\n} | {\n    type: \"ListFullProcedures\";\n    params: ListFullProcedures;\n} | {\n    type: \"GetActionsSummary\";\n    params: GetActionsSummary;\n} | {\n    type: \"GetAction\";\n    params: GetAction;\n} | {\n    type: \"GetActionActionState\";\n    params: GetActionActionState;\n} | {\n    type: \"ListActions\";\n    params: ListActions;\n} | {\n    type: \"ListFullActions\";\n    params: ListFullActions;\n} | {\n    type: \"ListSchedules\";\n    params: ListSchedules;\n} | {\n    type: \"GetServersSummary\";\n    params: GetServersSummary;\n} | {\n    type: \"GetServer\";\n    params: GetServer;\n} | {\n    type: \"GetServerState\";\n    params: GetServerState;\n} | {\n    type: \"GetPeripheryVersion\";\n    params: GetPeripheryVersion;\n} | {\n    type: \"GetServerActionState\";\n    params: GetServerActionState;\n} | {\n    type: \"GetHistoricalServerStats\";\n    params: GetHistoricalServerStats;\n} | {\n    type: \"ListServers\";\n    params: ListServers;\n} | {\n    type: \"ListFullServers\";\n    params: ListFullServers;\n} | {\n    type: \"InspectDockerContainer\";\n    params: InspectDockerContainer;\n} | {\n    type: \"GetResourceMatchingContainer\";\n    params: GetResourceMatchingContainer;\n} | {\n    type: \"GetContainerLog\";\n    params: GetContainerLog;\n} | {\n    type: \"SearchContainerLog\";\n    params: SearchContainerLog;\n} | {\n    type: \"InspectDockerNetwork\";\n    params: InspectDockerNetwork;\n} | {\n    type: \"InspectDockerImage\";\n    params: InspectDockerImage;\n} | {\n    type: \"ListDockerImageHistory\";\n    params: ListDockerImageHistory;\n} | {\n    type: \"InspectDockerVolume\";\n    params: InspectDockerVolume;\n} | {\n    type: \"GetDockerContainersSummary\";\n    params: GetDockerContainersSummary;\n} | {\n    type: \"ListAllDockerContainers\";\n    params: ListAllDockerContainers;\n} | {\n    type: \"ListDockerContainers\";\n    params: ListDockerContainers;\n} | {\n    type: \"ListDockerNetworks\";\n    params: ListDockerNetworks;\n} | {\n    type: \"ListDockerImages\";\n    params: ListDockerImages;\n} | {\n    type: \"ListDockerVolumes\";\n    params: ListDockerVolumes;\n} | {\n    type: \"ListComposeProjects\";\n    params: ListComposeProjects;\n} | {\n    type: \"ListTerminals\";\n    params: ListTerminals;\n} | {\n    type: \"GetSystemInformation\";\n    params: GetSystemInformation;\n} | {\n    type: \"GetSystemStats\";\n    params: GetSystemStats;\n} | {\n    type: \"ListSystemProcesses\";\n    params: ListSystemProcesses;\n} | {\n    type: \"GetStacksSummary\";\n    params: GetStacksSummary;\n} | {\n    type: \"GetStack\";\n    params: GetStack;\n} | {\n    type: \"GetStackActionState\";\n    params: GetStackActionState;\n} | {\n    type: \"GetStackWebhooksEnabled\";\n    params: GetStackWebhooksEnabled;\n} | {\n    type: \"GetStackLog\";\n    params: GetStackLog;\n} | {\n    type: \"SearchStackLog\";\n    params: SearchStackLog;\n} | {\n    type: \"InspectStackContainer\";\n    params: InspectStackContainer;\n} | {\n    type: \"ListStacks\";\n    params: ListStacks;\n} | {\n    type: \"ListFullStacks\";\n    params: ListFullStacks;\n} | {\n    type: \"ListStackServices\";\n    params: ListStackServices;\n} | {\n    type: \"ListCommonStackExtraArgs\";\n    params: ListCommonStackExtraArgs;\n} | {\n    type: \"ListCommonStackBuildExtraArgs\";\n    params: ListCommonStackBuildExtraArgs;\n} | {\n    type: \"GetDeploymentsSummary\";\n    params: GetDeploymentsSummary;\n} | {\n    type: \"GetDeployment\";\n    params: GetDeployment;\n} | {\n    type: \"GetDeploymentContainer\";\n    params: GetDeploymentContainer;\n} | {\n    type: \"GetDeploymentActionState\";\n    params: GetDeploymentActionState;\n} | {\n    type: \"GetDeploymentStats\";\n    params: GetDeploymentStats;\n} | {\n    type: \"GetDeploymentLog\";\n    params: GetDeploymentLog;\n} | {\n    type: \"SearchDeploymentLog\";\n    params: SearchDeploymentLog;\n} | {\n    type: \"InspectDeploymentContainer\";\n    params: InspectDeploymentContainer;\n} | {\n    type: \"ListDeployments\";\n    params: ListDeployments;\n} | {\n    type: \"ListFullDeployments\";\n    params: ListFullDeployments;\n} | {\n    type: \"ListCommonDeploymentExtraArgs\";\n    params: ListCommonDeploymentExtraArgs;\n} | {\n    type: \"GetBuildsSummary\";\n    params: GetBuildsSummary;\n} | {\n    type: \"GetBuild\";\n    params: GetBuild;\n} | {\n    type: \"GetBuildActionState\";\n    params: GetBuildActionState;\n} | {\n    type: \"GetBuildMonthlyStats\";\n    params: GetBuildMonthlyStats;\n} | {\n    type: \"ListBuildVersions\";\n    params: ListBuildVersions;\n} | {\n    type: \"GetBuildWebhookEnabled\";\n    params: GetBuildWebhookEnabled;\n} | {\n    type: \"ListBuilds\";\n    params: ListBuilds;\n} | {\n    type: \"ListFullBuilds\";\n    params: ListFullBuilds;\n} | {\n    type: \"ListCommonBuildExtraArgs\";\n    params: ListCommonBuildExtraArgs;\n} | {\n    type: \"GetReposSummary\";\n    params: GetReposSummary;\n} | {\n    type: \"GetRepo\";\n    params: GetRepo;\n} | {\n    type: \"GetRepoActionState\";\n    params: GetRepoActionState;\n} | {\n    type: \"GetRepoWebhooksEnabled\";\n    params: GetRepoWebhooksEnabled;\n} | {\n    type: \"ListRepos\";\n    params: ListRepos;\n} | {\n    type: \"ListFullRepos\";\n    params: ListFullRepos;\n} | {\n    type: \"GetResourceSyncsSummary\";\n    params: GetResourceSyncsSummary;\n} | {\n    type: \"GetResourceSync\";\n    params: GetResourceSync;\n} | {\n    type: \"GetResourceSyncActionState\";\n    params: GetResourceSyncActionState;\n} | {\n    type: \"GetSyncWebhooksEnabled\";\n    params: GetSyncWebhooksEnabled;\n} | {\n    type: \"ListResourceSyncs\";\n    params: ListResourceSyncs;\n} | {\n    type: \"ListFullResourceSyncs\";\n    params: ListFullResourceSyncs;\n} | {\n    type: \"GetBuildersSummary\";\n    params: GetBuildersSummary;\n} | {\n    type: \"GetBuilder\";\n    params: GetBuilder;\n} | {\n    type: \"ListBuilders\";\n    params: ListBuilders;\n} | {\n    type: \"ListFullBuilders\";\n    params: ListFullBuilders;\n} | {\n    type: \"GetAlertersSummary\";\n    params: GetAlertersSummary;\n} | {\n    type: \"GetAlerter\";\n    params: GetAlerter;\n} | {\n    type: \"ListAlerters\";\n    params: ListAlerters;\n} | {\n    type: \"ListFullAlerters\";\n    params: ListFullAlerters;\n} | {\n    type: \"ExportAllResourcesToToml\";\n    params: ExportAllResourcesToToml;\n} | {\n    type: \"ExportResourcesToToml\";\n    params: ExportResourcesToToml;\n} | {\n    type: \"GetTag\";\n    params: GetTag;\n} | {\n    type: \"ListTags\";\n    params: ListTags;\n} | {\n    type: \"GetUpdate\";\n    params: GetUpdate;\n} | {\n    type: \"ListUpdates\";\n    params: ListUpdates;\n} | {\n    type: \"ListAlerts\";\n    params: ListAlerts;\n} | {\n    type: \"GetAlert\";\n    params: GetAlert;\n} | {\n    type: \"GetVariable\";\n    params: GetVariable;\n} | {\n    type: \"ListVariables\";\n    params: ListVariables;\n} | {\n    type: \"GetGitProviderAccount\";\n    params: GetGitProviderAccount;\n} | {\n    type: \"ListGitProviderAccounts\";\n    params: ListGitProviderAccounts;\n} | {\n    type: \"GetDockerRegistryAccount\";\n    params: GetDockerRegistryAccount;\n} | {\n    type: \"ListDockerRegistryAccounts\";\n    params: ListDockerRegistryAccounts;\n};\n/** The specific types of permission that a User or UserGroup can have on a resource. */\nexport declare enum SpecificPermission {\n    /**\n     * On **Server**\n     * - Access the terminal apis\n     * On **Stack / Deployment**\n     * - Access the container exec Apis\n     */\n    Terminal = \"Terminal\",\n    /**\n     * On **Server**\n     * - Allowed to attach Stacks, Deployments, Repos, Builders to the Server\n     * On **Builder**\n     * - Allowed to attach Builds to the Builder\n     * On **Build**\n     * - Allowed to attach Deployments to the Build\n     */\n    Attach = \"Attach\",\n    /**\n     * On **Server**\n     * - Access the `container inspect` apis\n     * On **Stack / Deployment**\n     * - Access `container inspect` apis for associated containers\n     */\n    Inspect = \"Inspect\",\n    /**\n     * On **Server**\n     * - Read all container logs on the server\n     * On **Stack / Deployment**\n     * - Read the container logs\n     */\n    Logs = \"Logs\",\n    /**\n     * On **Server**\n     * - Read all the processes on the host\n     */\n    Processes = \"Processes\"\n}\nexport type UserRequest = {\n    type: \"PushRecentlyViewed\";\n    params: PushRecentlyViewed;\n} | {\n    type: \"SetLastSeenUpdate\";\n    params: SetLastSeenUpdate;\n} | {\n    type: \"CreateApiKey\";\n    params: CreateApiKey;\n} | {\n    type: \"DeleteApiKey\";\n    params: DeleteApiKey;\n};\nexport type WriteRequest = {\n    type: \"CreateLocalUser\";\n    params: CreateLocalUser;\n} | {\n    type: \"UpdateUserUsername\";\n    params: UpdateUserUsername;\n} | {\n    type: \"UpdateUserPassword\";\n    params: UpdateUserPassword;\n} | {\n    type: \"DeleteUser\";\n    params: DeleteUser;\n} | {\n    type: \"CreateServiceUser\";\n    params: CreateServiceUser;\n} | {\n    type: \"UpdateServiceUserDescription\";\n    params: UpdateServiceUserDescription;\n} | {\n    type: \"CreateApiKeyForServiceUser\";\n    params: CreateApiKeyForServiceUser;\n} | {\n    type: \"DeleteApiKeyForServiceUser\";\n    params: DeleteApiKeyForServiceUser;\n} | {\n    type: \"CreateUserGroup\";\n    params: CreateUserGroup;\n} | {\n    type: \"RenameUserGroup\";\n    params: RenameUserGroup;\n} | {\n    type: \"DeleteUserGroup\";\n    params: DeleteUserGroup;\n} | {\n    type: \"AddUserToUserGroup\";\n    params: AddUserToUserGroup;\n} | {\n    type: \"RemoveUserFromUserGroup\";\n    params: RemoveUserFromUserGroup;\n} | {\n    type: \"SetUsersInUserGroup\";\n    params: SetUsersInUserGroup;\n} | {\n    type: \"SetEveryoneUserGroup\";\n    params: SetEveryoneUserGroup;\n} | {\n    type: \"UpdateUserAdmin\";\n    params: UpdateUserAdmin;\n} | {\n    type: \"UpdateUserBasePermissions\";\n    params: UpdateUserBasePermissions;\n} | {\n    type: \"UpdatePermissionOnResourceType\";\n    params: UpdatePermissionOnResourceType;\n} | {\n    type: \"UpdatePermissionOnTarget\";\n    params: UpdatePermissionOnTarget;\n} | {\n    type: \"UpdateResourceMeta\";\n    params: UpdateResourceMeta;\n} | {\n    type: \"CreateServer\";\n    params: CreateServer;\n} | {\n    type: \"CopyServer\";\n    params: CopyServer;\n} | {\n    type: \"DeleteServer\";\n    params: DeleteServer;\n} | {\n    type: \"UpdateServer\";\n    params: UpdateServer;\n} | {\n    type: \"RenameServer\";\n    params: RenameServer;\n} | {\n    type: \"CreateNetwork\";\n    params: CreateNetwork;\n} | {\n    type: \"CreateTerminal\";\n    params: CreateTerminal;\n} | {\n    type: \"DeleteTerminal\";\n    params: DeleteTerminal;\n} | {\n    type: \"DeleteAllTerminals\";\n    params: DeleteAllTerminals;\n} | {\n    type: \"CreateStack\";\n    params: CreateStack;\n} | {\n    type: \"CopyStack\";\n    params: CopyStack;\n} | {\n    type: \"DeleteStack\";\n    params: DeleteStack;\n} | {\n    type: \"UpdateStack\";\n    params: UpdateStack;\n} | {\n    type: \"RenameStack\";\n    params: RenameStack;\n} | {\n    type: \"WriteStackFileContents\";\n    params: WriteStackFileContents;\n} | {\n    type: \"RefreshStackCache\";\n    params: RefreshStackCache;\n} | {\n    type: \"CreateStackWebhook\";\n    params: CreateStackWebhook;\n} | {\n    type: \"DeleteStackWebhook\";\n    params: DeleteStackWebhook;\n} | {\n    type: \"CreateDeployment\";\n    params: CreateDeployment;\n} | {\n    type: \"CopyDeployment\";\n    params: CopyDeployment;\n} | {\n    type: \"CreateDeploymentFromContainer\";\n    params: CreateDeploymentFromContainer;\n} | {\n    type: \"DeleteDeployment\";\n    params: DeleteDeployment;\n} | {\n    type: \"UpdateDeployment\";\n    params: UpdateDeployment;\n} | {\n    type: \"RenameDeployment\";\n    params: RenameDeployment;\n} | {\n    type: \"CreateBuild\";\n    params: CreateBuild;\n} | {\n    type: \"CopyBuild\";\n    params: CopyBuild;\n} | {\n    type: \"DeleteBuild\";\n    params: DeleteBuild;\n} | {\n    type: \"UpdateBuild\";\n    params: UpdateBuild;\n} | {\n    type: \"RenameBuild\";\n    params: RenameBuild;\n} | {\n    type: \"WriteBuildFileContents\";\n    params: WriteBuildFileContents;\n} | {\n    type: \"RefreshBuildCache\";\n    params: RefreshBuildCache;\n} | {\n    type: \"CreateBuildWebhook\";\n    params: CreateBuildWebhook;\n} | {\n    type: \"DeleteBuildWebhook\";\n    params: DeleteBuildWebhook;\n} | {\n    type: \"CreateBuilder\";\n    params: CreateBuilder;\n} | {\n    type: \"CopyBuilder\";\n    params: CopyBuilder;\n} | {\n    type: \"DeleteBuilder\";\n    params: DeleteBuilder;\n} | {\n    type: \"UpdateBuilder\";\n    params: UpdateBuilder;\n} | {\n    type: \"RenameBuilder\";\n    params: RenameBuilder;\n} | {\n    type: \"CreateRepo\";\n    params: CreateRepo;\n} | {\n    type: \"CopyRepo\";\n    params: CopyRepo;\n} | {\n    type: \"DeleteRepo\";\n    params: DeleteRepo;\n} | {\n    type: \"UpdateRepo\";\n    params: UpdateRepo;\n} | {\n    type: \"RenameRepo\";\n    params: RenameRepo;\n} | {\n    type: \"RefreshRepoCache\";\n    params: RefreshRepoCache;\n} | {\n    type: \"CreateRepoWebhook\";\n    params: CreateRepoWebhook;\n} | {\n    type: \"DeleteRepoWebhook\";\n    params: DeleteRepoWebhook;\n} | {\n    type: \"CreateAlerter\";\n    params: CreateAlerter;\n} | {\n    type: \"CopyAlerter\";\n    params: CopyAlerter;\n} | {\n    type: \"DeleteAlerter\";\n    params: DeleteAlerter;\n} | {\n    type: \"UpdateAlerter\";\n    params: UpdateAlerter;\n} | {\n    type: \"RenameAlerter\";\n    params: RenameAlerter;\n} | {\n    type: \"CreateProcedure\";\n    params: CreateProcedure;\n} | {\n    type: \"CopyProcedure\";\n    params: CopyProcedure;\n} | {\n    type: \"DeleteProcedure\";\n    params: DeleteProcedure;\n} | {\n    type: \"UpdateProcedure\";\n    params: UpdateProcedure;\n} | {\n    type: \"RenameProcedure\";\n    params: RenameProcedure;\n} | {\n    type: \"CreateAction\";\n    params: CreateAction;\n} | {\n    type: \"CopyAction\";\n    params: CopyAction;\n} | {\n    type: \"DeleteAction\";\n    params: DeleteAction;\n} | {\n    type: \"UpdateAction\";\n    params: UpdateAction;\n} | {\n    type: \"RenameAction\";\n    params: RenameAction;\n} | {\n    type: \"CreateResourceSync\";\n    params: CreateResourceSync;\n} | {\n    type: \"CopyResourceSync\";\n    params: CopyResourceSync;\n} | {\n    type: \"DeleteResourceSync\";\n    params: DeleteResourceSync;\n} | {\n    type: \"UpdateResourceSync\";\n    params: UpdateResourceSync;\n} | {\n    type: \"RenameResourceSync\";\n    params: RenameResourceSync;\n} | {\n    type: \"WriteSyncFileContents\";\n    params: WriteSyncFileContents;\n} | {\n    type: \"CommitSync\";\n    params: CommitSync;\n} | {\n    type: \"RefreshResourceSyncPending\";\n    params: RefreshResourceSyncPending;\n} | {\n    type: \"CreateSyncWebhook\";\n    params: CreateSyncWebhook;\n} | {\n    type: \"DeleteSyncWebhook\";\n    params: DeleteSyncWebhook;\n} | {\n    type: \"CreateTag\";\n    params: CreateTag;\n} | {\n    type: \"DeleteTag\";\n    params: DeleteTag;\n} | {\n    type: \"RenameTag\";\n    params: RenameTag;\n} | {\n    type: \"UpdateTagColor\";\n    params: UpdateTagColor;\n} | {\n    type: \"CreateVariable\";\n    params: CreateVariable;\n} | {\n    type: \"UpdateVariableValue\";\n    params: UpdateVariableValue;\n} | {\n    type: \"UpdateVariableDescription\";\n    params: UpdateVariableDescription;\n} | {\n    type: \"UpdateVariableIsSecret\";\n    params: UpdateVariableIsSecret;\n} | {\n    type: \"DeleteVariable\";\n    params: DeleteVariable;\n} | {\n    type: \"CreateGitProviderAccount\";\n    params: CreateGitProviderAccount;\n} | {\n    type: \"UpdateGitProviderAccount\";\n    params: UpdateGitProviderAccount;\n} | {\n    type: \"DeleteGitProviderAccount\";\n    params: DeleteGitProviderAccount;\n} | {\n    type: \"CreateDockerRegistryAccount\";\n    params: CreateDockerRegistryAccount;\n} | {\n    type: \"UpdateDockerRegistryAccount\";\n    params: UpdateDockerRegistryAccount;\n} | {\n    type: \"DeleteDockerRegistryAccount\";\n    params: DeleteDockerRegistryAccount;\n};\nexport type WsLoginMessage = {\n    type: \"Jwt\";\n    params: {\n        jwt: string;\n    };\n} | {\n    type: \"ApiKeys\";\n    params: {\n        key: string;\n        secret: string;\n    };\n};\n"
  },
  {
    "path": "frontend/public/client/types.js",
    "content": "/*\n Generated by typeshare 1.13.3\n*/\n/** The levels of permission that a User or UserGroup can have on a resource. */\nexport var PermissionLevel;\n(function (PermissionLevel) {\n    /** No permissions. */\n    PermissionLevel[\"None\"] = \"None\";\n    /** Can read resource information and config */\n    PermissionLevel[\"Read\"] = \"Read\";\n    /** Can execute actions on the resource */\n    PermissionLevel[\"Execute\"] = \"Execute\";\n    /** Can update the resource configuration */\n    PermissionLevel[\"Write\"] = \"Write\";\n})(PermissionLevel || (PermissionLevel = {}));\nexport var ScheduleFormat;\n(function (ScheduleFormat) {\n    ScheduleFormat[\"English\"] = \"English\";\n    ScheduleFormat[\"Cron\"] = \"Cron\";\n})(ScheduleFormat || (ScheduleFormat = {}));\nexport var FileFormat;\n(function (FileFormat) {\n    FileFormat[\"KeyValue\"] = \"key_value\";\n    FileFormat[\"Toml\"] = \"toml\";\n    FileFormat[\"Yaml\"] = \"yaml\";\n    FileFormat[\"Json\"] = \"json\";\n})(FileFormat || (FileFormat = {}));\nexport var ActionState;\n(function (ActionState) {\n    /** Unknown case */\n    ActionState[\"Unknown\"] = \"Unknown\";\n    /** Last clone / pull successful (or never cloned) */\n    ActionState[\"Ok\"] = \"Ok\";\n    /** Last clone / pull failed */\n    ActionState[\"Failed\"] = \"Failed\";\n    /** Currently running */\n    ActionState[\"Running\"] = \"Running\";\n})(ActionState || (ActionState = {}));\nexport var TemplatesQueryBehavior;\n(function (TemplatesQueryBehavior) {\n    /** Include templates in results. Default. */\n    TemplatesQueryBehavior[\"Include\"] = \"Include\";\n    /** Exclude templates from results. */\n    TemplatesQueryBehavior[\"Exclude\"] = \"Exclude\";\n    /** Results *only* includes templates. */\n    TemplatesQueryBehavior[\"Only\"] = \"Only\";\n})(TemplatesQueryBehavior || (TemplatesQueryBehavior = {}));\nexport var TagQueryBehavior;\n(function (TagQueryBehavior) {\n    /** Returns resources which have strictly all the tags */\n    TagQueryBehavior[\"All\"] = \"All\";\n    /** Returns resources which have one or more of the tags */\n    TagQueryBehavior[\"Any\"] = \"Any\";\n})(TagQueryBehavior || (TagQueryBehavior = {}));\n/** Types of maintenance schedules */\nexport var MaintenanceScheduleType;\n(function (MaintenanceScheduleType) {\n    /** Daily at the specified time */\n    MaintenanceScheduleType[\"Daily\"] = \"Daily\";\n    /** Weekly on the specified day and time */\n    MaintenanceScheduleType[\"Weekly\"] = \"Weekly\";\n    /** One-time maintenance on a specific date and time */\n    MaintenanceScheduleType[\"OneTime\"] = \"OneTime\";\n})(MaintenanceScheduleType || (MaintenanceScheduleType = {}));\nexport var Operation;\n(function (Operation) {\n    Operation[\"None\"] = \"None\";\n    Operation[\"CreateServer\"] = \"CreateServer\";\n    Operation[\"UpdateServer\"] = \"UpdateServer\";\n    Operation[\"DeleteServer\"] = \"DeleteServer\";\n    Operation[\"RenameServer\"] = \"RenameServer\";\n    Operation[\"StartContainer\"] = \"StartContainer\";\n    Operation[\"RestartContainer\"] = \"RestartContainer\";\n    Operation[\"PauseContainer\"] = \"PauseContainer\";\n    Operation[\"UnpauseContainer\"] = \"UnpauseContainer\";\n    Operation[\"StopContainer\"] = \"StopContainer\";\n    Operation[\"DestroyContainer\"] = \"DestroyContainer\";\n    Operation[\"StartAllContainers\"] = \"StartAllContainers\";\n    Operation[\"RestartAllContainers\"] = \"RestartAllContainers\";\n    Operation[\"PauseAllContainers\"] = \"PauseAllContainers\";\n    Operation[\"UnpauseAllContainers\"] = \"UnpauseAllContainers\";\n    Operation[\"StopAllContainers\"] = \"StopAllContainers\";\n    Operation[\"PruneContainers\"] = \"PruneContainers\";\n    Operation[\"CreateNetwork\"] = \"CreateNetwork\";\n    Operation[\"DeleteNetwork\"] = \"DeleteNetwork\";\n    Operation[\"PruneNetworks\"] = \"PruneNetworks\";\n    Operation[\"DeleteImage\"] = \"DeleteImage\";\n    Operation[\"PruneImages\"] = \"PruneImages\";\n    Operation[\"DeleteVolume\"] = \"DeleteVolume\";\n    Operation[\"PruneVolumes\"] = \"PruneVolumes\";\n    Operation[\"PruneDockerBuilders\"] = \"PruneDockerBuilders\";\n    Operation[\"PruneBuildx\"] = \"PruneBuildx\";\n    Operation[\"PruneSystem\"] = \"PruneSystem\";\n    Operation[\"CreateStack\"] = \"CreateStack\";\n    Operation[\"UpdateStack\"] = \"UpdateStack\";\n    Operation[\"RenameStack\"] = \"RenameStack\";\n    Operation[\"DeleteStack\"] = \"DeleteStack\";\n    Operation[\"WriteStackContents\"] = \"WriteStackContents\";\n    Operation[\"RefreshStackCache\"] = \"RefreshStackCache\";\n    Operation[\"PullStack\"] = \"PullStack\";\n    Operation[\"DeployStack\"] = \"DeployStack\";\n    Operation[\"StartStack\"] = \"StartStack\";\n    Operation[\"RestartStack\"] = \"RestartStack\";\n    Operation[\"PauseStack\"] = \"PauseStack\";\n    Operation[\"UnpauseStack\"] = \"UnpauseStack\";\n    Operation[\"StopStack\"] = \"StopStack\";\n    Operation[\"DestroyStack\"] = \"DestroyStack\";\n    Operation[\"RunStackService\"] = \"RunStackService\";\n    Operation[\"DeployStackService\"] = \"DeployStackService\";\n    Operation[\"PullStackService\"] = \"PullStackService\";\n    Operation[\"StartStackService\"] = \"StartStackService\";\n    Operation[\"RestartStackService\"] = \"RestartStackService\";\n    Operation[\"PauseStackService\"] = \"PauseStackService\";\n    Operation[\"UnpauseStackService\"] = \"UnpauseStackService\";\n    Operation[\"StopStackService\"] = \"StopStackService\";\n    Operation[\"DestroyStackService\"] = \"DestroyStackService\";\n    Operation[\"CreateDeployment\"] = \"CreateDeployment\";\n    Operation[\"UpdateDeployment\"] = \"UpdateDeployment\";\n    Operation[\"RenameDeployment\"] = \"RenameDeployment\";\n    Operation[\"DeleteDeployment\"] = \"DeleteDeployment\";\n    Operation[\"Deploy\"] = \"Deploy\";\n    Operation[\"PullDeployment\"] = \"PullDeployment\";\n    Operation[\"StartDeployment\"] = \"StartDeployment\";\n    Operation[\"RestartDeployment\"] = \"RestartDeployment\";\n    Operation[\"PauseDeployment\"] = \"PauseDeployment\";\n    Operation[\"UnpauseDeployment\"] = \"UnpauseDeployment\";\n    Operation[\"StopDeployment\"] = \"StopDeployment\";\n    Operation[\"DestroyDeployment\"] = \"DestroyDeployment\";\n    Operation[\"CreateBuild\"] = \"CreateBuild\";\n    Operation[\"UpdateBuild\"] = \"UpdateBuild\";\n    Operation[\"RenameBuild\"] = \"RenameBuild\";\n    Operation[\"DeleteBuild\"] = \"DeleteBuild\";\n    Operation[\"RunBuild\"] = \"RunBuild\";\n    Operation[\"CancelBuild\"] = \"CancelBuild\";\n    Operation[\"WriteDockerfile\"] = \"WriteDockerfile\";\n    Operation[\"CreateRepo\"] = \"CreateRepo\";\n    Operation[\"UpdateRepo\"] = \"UpdateRepo\";\n    Operation[\"RenameRepo\"] = \"RenameRepo\";\n    Operation[\"DeleteRepo\"] = \"DeleteRepo\";\n    Operation[\"CloneRepo\"] = \"CloneRepo\";\n    Operation[\"PullRepo\"] = \"PullRepo\";\n    Operation[\"BuildRepo\"] = \"BuildRepo\";\n    Operation[\"CancelRepoBuild\"] = \"CancelRepoBuild\";\n    Operation[\"CreateProcedure\"] = \"CreateProcedure\";\n    Operation[\"UpdateProcedure\"] = \"UpdateProcedure\";\n    Operation[\"RenameProcedure\"] = \"RenameProcedure\";\n    Operation[\"DeleteProcedure\"] = \"DeleteProcedure\";\n    Operation[\"RunProcedure\"] = \"RunProcedure\";\n    Operation[\"CreateAction\"] = \"CreateAction\";\n    Operation[\"UpdateAction\"] = \"UpdateAction\";\n    Operation[\"RenameAction\"] = \"RenameAction\";\n    Operation[\"DeleteAction\"] = \"DeleteAction\";\n    Operation[\"RunAction\"] = \"RunAction\";\n    Operation[\"CreateBuilder\"] = \"CreateBuilder\";\n    Operation[\"UpdateBuilder\"] = \"UpdateBuilder\";\n    Operation[\"RenameBuilder\"] = \"RenameBuilder\";\n    Operation[\"DeleteBuilder\"] = \"DeleteBuilder\";\n    Operation[\"CreateAlerter\"] = \"CreateAlerter\";\n    Operation[\"UpdateAlerter\"] = \"UpdateAlerter\";\n    Operation[\"RenameAlerter\"] = \"RenameAlerter\";\n    Operation[\"DeleteAlerter\"] = \"DeleteAlerter\";\n    Operation[\"TestAlerter\"] = \"TestAlerter\";\n    Operation[\"SendAlert\"] = \"SendAlert\";\n    Operation[\"CreateResourceSync\"] = \"CreateResourceSync\";\n    Operation[\"UpdateResourceSync\"] = \"UpdateResourceSync\";\n    Operation[\"RenameResourceSync\"] = \"RenameResourceSync\";\n    Operation[\"DeleteResourceSync\"] = \"DeleteResourceSync\";\n    Operation[\"WriteSyncContents\"] = \"WriteSyncContents\";\n    Operation[\"CommitSync\"] = \"CommitSync\";\n    Operation[\"RunSync\"] = \"RunSync\";\n    Operation[\"ClearRepoCache\"] = \"ClearRepoCache\";\n    Operation[\"BackupCoreDatabase\"] = \"BackupCoreDatabase\";\n    Operation[\"GlobalAutoUpdate\"] = \"GlobalAutoUpdate\";\n    Operation[\"CreateVariable\"] = \"CreateVariable\";\n    Operation[\"UpdateVariableValue\"] = \"UpdateVariableValue\";\n    Operation[\"DeleteVariable\"] = \"DeleteVariable\";\n    Operation[\"CreateGitProviderAccount\"] = \"CreateGitProviderAccount\";\n    Operation[\"UpdateGitProviderAccount\"] = \"UpdateGitProviderAccount\";\n    Operation[\"DeleteGitProviderAccount\"] = \"DeleteGitProviderAccount\";\n    Operation[\"CreateDockerRegistryAccount\"] = \"CreateDockerRegistryAccount\";\n    Operation[\"UpdateDockerRegistryAccount\"] = \"UpdateDockerRegistryAccount\";\n    Operation[\"DeleteDockerRegistryAccount\"] = \"DeleteDockerRegistryAccount\";\n})(Operation || (Operation = {}));\n/** An update's status */\nexport var UpdateStatus;\n(function (UpdateStatus) {\n    /** The run is in the system but hasn't started yet */\n    UpdateStatus[\"Queued\"] = \"Queued\";\n    /** The run is currently running */\n    UpdateStatus[\"InProgress\"] = \"InProgress\";\n    /** The run is complete */\n    UpdateStatus[\"Complete\"] = \"Complete\";\n})(UpdateStatus || (UpdateStatus = {}));\nexport var BuildState;\n(function (BuildState) {\n    /** Currently building */\n    BuildState[\"Building\"] = \"Building\";\n    /** Last build successful (or never built) */\n    BuildState[\"Ok\"] = \"Ok\";\n    /** Last build failed */\n    BuildState[\"Failed\"] = \"Failed\";\n    /** Other case */\n    BuildState[\"Unknown\"] = \"Unknown\";\n})(BuildState || (BuildState = {}));\nexport var RestartMode;\n(function (RestartMode) {\n    RestartMode[\"NoRestart\"] = \"no\";\n    RestartMode[\"OnFailure\"] = \"on-failure\";\n    RestartMode[\"Always\"] = \"always\";\n    RestartMode[\"UnlessStopped\"] = \"unless-stopped\";\n})(RestartMode || (RestartMode = {}));\nexport var TerminationSignal;\n(function (TerminationSignal) {\n    TerminationSignal[\"SigHup\"] = \"SIGHUP\";\n    TerminationSignal[\"SigInt\"] = \"SIGINT\";\n    TerminationSignal[\"SigQuit\"] = \"SIGQUIT\";\n    TerminationSignal[\"SigTerm\"] = \"SIGTERM\";\n})(TerminationSignal || (TerminationSignal = {}));\n/**\n * Variants de/serialized from/to snake_case.\n *\n * Eg.\n * - NotDeployed -> not_deployed\n * - Restarting -> restarting\n * - Running -> running.\n */\nexport var DeploymentState;\n(function (DeploymentState) {\n    /** The deployment is currently re/deploying */\n    DeploymentState[\"Deploying\"] = \"deploying\";\n    /** Container is running */\n    DeploymentState[\"Running\"] = \"running\";\n    /** Container is created but not running */\n    DeploymentState[\"Created\"] = \"created\";\n    /** Container is in restart loop */\n    DeploymentState[\"Restarting\"] = \"restarting\";\n    /** Container is being removed */\n    DeploymentState[\"Removing\"] = \"removing\";\n    /** Container is paused */\n    DeploymentState[\"Paused\"] = \"paused\";\n    /** Container is exited */\n    DeploymentState[\"Exited\"] = \"exited\";\n    /** Container is dead */\n    DeploymentState[\"Dead\"] = \"dead\";\n    /** The deployment is not deployed (no matching container) */\n    DeploymentState[\"NotDeployed\"] = \"not_deployed\";\n    /** Server not reachable for status */\n    DeploymentState[\"Unknown\"] = \"unknown\";\n})(DeploymentState || (DeploymentState = {}));\n/** Severity level of problem. */\nexport var SeverityLevel;\n(function (SeverityLevel) {\n    /**\n     * No problem.\n     *\n     * Aliases: ok, low, l\n     */\n    SeverityLevel[\"Ok\"] = \"OK\";\n    /**\n     * Problem is imminent.\n     *\n     * Aliases: warning, w, medium, m\n     */\n    SeverityLevel[\"Warning\"] = \"WARNING\";\n    /**\n     * Problem fully realized.\n     *\n     * Aliases: critical, c, high, h\n     */\n    SeverityLevel[\"Critical\"] = \"CRITICAL\";\n})(SeverityLevel || (SeverityLevel = {}));\nexport var StackFileRequires;\n(function (StackFileRequires) {\n    /** Diff requires service redeploy. */\n    StackFileRequires[\"Redeploy\"] = \"Redeploy\";\n    /** Diff requires service restart */\n    StackFileRequires[\"Restart\"] = \"Restart\";\n    /** Diff requires no action. Default. */\n    StackFileRequires[\"None\"] = \"None\";\n})(StackFileRequires || (StackFileRequires = {}));\nexport var Timelength;\n(function (Timelength) {\n    /** `1-sec` */\n    Timelength[\"OneSecond\"] = \"1-sec\";\n    /** `5-sec` */\n    Timelength[\"FiveSeconds\"] = \"5-sec\";\n    /** `10-sec` */\n    Timelength[\"TenSeconds\"] = \"10-sec\";\n    /** `15-sec` */\n    Timelength[\"FifteenSeconds\"] = \"15-sec\";\n    /** `30-sec` */\n    Timelength[\"ThirtySeconds\"] = \"30-sec\";\n    /** `1-min` */\n    Timelength[\"OneMinute\"] = \"1-min\";\n    /** `2-min` */\n    Timelength[\"TwoMinutes\"] = \"2-min\";\n    /** `5-min` */\n    Timelength[\"FiveMinutes\"] = \"5-min\";\n    /** `10-min` */\n    Timelength[\"TenMinutes\"] = \"10-min\";\n    /** `15-min` */\n    Timelength[\"FifteenMinutes\"] = \"15-min\";\n    /** `30-min` */\n    Timelength[\"ThirtyMinutes\"] = \"30-min\";\n    /** `1-hr` */\n    Timelength[\"OneHour\"] = \"1-hr\";\n    /** `2-hr` */\n    Timelength[\"TwoHours\"] = \"2-hr\";\n    /** `6-hr` */\n    Timelength[\"SixHours\"] = \"6-hr\";\n    /** `8-hr` */\n    Timelength[\"EightHours\"] = \"8-hr\";\n    /** `12-hr` */\n    Timelength[\"TwelveHours\"] = \"12-hr\";\n    /** `1-day` */\n    Timelength[\"OneDay\"] = \"1-day\";\n    /** `3-day` */\n    Timelength[\"ThreeDay\"] = \"3-day\";\n    /** `1-wk` */\n    Timelength[\"OneWeek\"] = \"1-wk\";\n    /** `2-wk` */\n    Timelength[\"TwoWeeks\"] = \"2-wk\";\n    /** `30-day` */\n    Timelength[\"ThirtyDays\"] = \"30-day\";\n})(Timelength || (Timelength = {}));\nexport var TagColor;\n(function (TagColor) {\n    TagColor[\"LightSlate\"] = \"LightSlate\";\n    TagColor[\"Slate\"] = \"Slate\";\n    TagColor[\"DarkSlate\"] = \"DarkSlate\";\n    TagColor[\"LightRed\"] = \"LightRed\";\n    TagColor[\"Red\"] = \"Red\";\n    TagColor[\"DarkRed\"] = \"DarkRed\";\n    TagColor[\"LightOrange\"] = \"LightOrange\";\n    TagColor[\"Orange\"] = \"Orange\";\n    TagColor[\"DarkOrange\"] = \"DarkOrange\";\n    TagColor[\"LightAmber\"] = \"LightAmber\";\n    TagColor[\"Amber\"] = \"Amber\";\n    TagColor[\"DarkAmber\"] = \"DarkAmber\";\n    TagColor[\"LightYellow\"] = \"LightYellow\";\n    TagColor[\"Yellow\"] = \"Yellow\";\n    TagColor[\"DarkYellow\"] = \"DarkYellow\";\n    TagColor[\"LightLime\"] = \"LightLime\";\n    TagColor[\"Lime\"] = \"Lime\";\n    TagColor[\"DarkLime\"] = \"DarkLime\";\n    TagColor[\"LightGreen\"] = \"LightGreen\";\n    TagColor[\"Green\"] = \"Green\";\n    TagColor[\"DarkGreen\"] = \"DarkGreen\";\n    TagColor[\"LightEmerald\"] = \"LightEmerald\";\n    TagColor[\"Emerald\"] = \"Emerald\";\n    TagColor[\"DarkEmerald\"] = \"DarkEmerald\";\n    TagColor[\"LightTeal\"] = \"LightTeal\";\n    TagColor[\"Teal\"] = \"Teal\";\n    TagColor[\"DarkTeal\"] = \"DarkTeal\";\n    TagColor[\"LightCyan\"] = \"LightCyan\";\n    TagColor[\"Cyan\"] = \"Cyan\";\n    TagColor[\"DarkCyan\"] = \"DarkCyan\";\n    TagColor[\"LightSky\"] = \"LightSky\";\n    TagColor[\"Sky\"] = \"Sky\";\n    TagColor[\"DarkSky\"] = \"DarkSky\";\n    TagColor[\"LightBlue\"] = \"LightBlue\";\n    TagColor[\"Blue\"] = \"Blue\";\n    TagColor[\"DarkBlue\"] = \"DarkBlue\";\n    TagColor[\"LightIndigo\"] = \"LightIndigo\";\n    TagColor[\"Indigo\"] = \"Indigo\";\n    TagColor[\"DarkIndigo\"] = \"DarkIndigo\";\n    TagColor[\"LightViolet\"] = \"LightViolet\";\n    TagColor[\"Violet\"] = \"Violet\";\n    TagColor[\"DarkViolet\"] = \"DarkViolet\";\n    TagColor[\"LightPurple\"] = \"LightPurple\";\n    TagColor[\"Purple\"] = \"Purple\";\n    TagColor[\"DarkPurple\"] = \"DarkPurple\";\n    TagColor[\"LightFuchsia\"] = \"LightFuchsia\";\n    TagColor[\"Fuchsia\"] = \"Fuchsia\";\n    TagColor[\"DarkFuchsia\"] = \"DarkFuchsia\";\n    TagColor[\"LightPink\"] = \"LightPink\";\n    TagColor[\"Pink\"] = \"Pink\";\n    TagColor[\"DarkPink\"] = \"DarkPink\";\n    TagColor[\"LightRose\"] = \"LightRose\";\n    TagColor[\"Rose\"] = \"Rose\";\n    TagColor[\"DarkRose\"] = \"DarkRose\";\n})(TagColor || (TagColor = {}));\nexport var ContainerStateStatusEnum;\n(function (ContainerStateStatusEnum) {\n    ContainerStateStatusEnum[\"Running\"] = \"running\";\n    ContainerStateStatusEnum[\"Created\"] = \"created\";\n    ContainerStateStatusEnum[\"Paused\"] = \"paused\";\n    ContainerStateStatusEnum[\"Restarting\"] = \"restarting\";\n    ContainerStateStatusEnum[\"Exited\"] = \"exited\";\n    ContainerStateStatusEnum[\"Removing\"] = \"removing\";\n    ContainerStateStatusEnum[\"Dead\"] = \"dead\";\n    ContainerStateStatusEnum[\"Empty\"] = \"\";\n})(ContainerStateStatusEnum || (ContainerStateStatusEnum = {}));\nexport var HealthStatusEnum;\n(function (HealthStatusEnum) {\n    HealthStatusEnum[\"Empty\"] = \"\";\n    HealthStatusEnum[\"None\"] = \"none\";\n    HealthStatusEnum[\"Starting\"] = \"starting\";\n    HealthStatusEnum[\"Healthy\"] = \"healthy\";\n    HealthStatusEnum[\"Unhealthy\"] = \"unhealthy\";\n})(HealthStatusEnum || (HealthStatusEnum = {}));\nexport var RestartPolicyNameEnum;\n(function (RestartPolicyNameEnum) {\n    RestartPolicyNameEnum[\"Empty\"] = \"\";\n    RestartPolicyNameEnum[\"No\"] = \"no\";\n    RestartPolicyNameEnum[\"Always\"] = \"always\";\n    RestartPolicyNameEnum[\"UnlessStopped\"] = \"unless-stopped\";\n    RestartPolicyNameEnum[\"OnFailure\"] = \"on-failure\";\n})(RestartPolicyNameEnum || (RestartPolicyNameEnum = {}));\nexport var MountTypeEnum;\n(function (MountTypeEnum) {\n    MountTypeEnum[\"Empty\"] = \"\";\n    MountTypeEnum[\"Bind\"] = \"bind\";\n    MountTypeEnum[\"Volume\"] = \"volume\";\n    MountTypeEnum[\"Image\"] = \"image\";\n    MountTypeEnum[\"Tmpfs\"] = \"tmpfs\";\n    MountTypeEnum[\"Npipe\"] = \"npipe\";\n    MountTypeEnum[\"Cluster\"] = \"cluster\";\n})(MountTypeEnum || (MountTypeEnum = {}));\nexport var MountBindOptionsPropagationEnum;\n(function (MountBindOptionsPropagationEnum) {\n    MountBindOptionsPropagationEnum[\"Empty\"] = \"\";\n    MountBindOptionsPropagationEnum[\"Private\"] = \"private\";\n    MountBindOptionsPropagationEnum[\"Rprivate\"] = \"rprivate\";\n    MountBindOptionsPropagationEnum[\"Shared\"] = \"shared\";\n    MountBindOptionsPropagationEnum[\"Rshared\"] = \"rshared\";\n    MountBindOptionsPropagationEnum[\"Slave\"] = \"slave\";\n    MountBindOptionsPropagationEnum[\"Rslave\"] = \"rslave\";\n})(MountBindOptionsPropagationEnum || (MountBindOptionsPropagationEnum = {}));\nexport var HostConfigCgroupnsModeEnum;\n(function (HostConfigCgroupnsModeEnum) {\n    HostConfigCgroupnsModeEnum[\"Empty\"] = \"\";\n    HostConfigCgroupnsModeEnum[\"Private\"] = \"private\";\n    HostConfigCgroupnsModeEnum[\"Host\"] = \"host\";\n})(HostConfigCgroupnsModeEnum || (HostConfigCgroupnsModeEnum = {}));\nexport var HostConfigIsolationEnum;\n(function (HostConfigIsolationEnum) {\n    HostConfigIsolationEnum[\"Empty\"] = \"\";\n    HostConfigIsolationEnum[\"Default\"] = \"default\";\n    HostConfigIsolationEnum[\"Process\"] = \"process\";\n    HostConfigIsolationEnum[\"Hyperv\"] = \"hyperv\";\n})(HostConfigIsolationEnum || (HostConfigIsolationEnum = {}));\nexport var VolumeScopeEnum;\n(function (VolumeScopeEnum) {\n    VolumeScopeEnum[\"Empty\"] = \"\";\n    VolumeScopeEnum[\"Local\"] = \"local\";\n    VolumeScopeEnum[\"Global\"] = \"global\";\n})(VolumeScopeEnum || (VolumeScopeEnum = {}));\nexport var ClusterVolumeSpecAccessModeScopeEnum;\n(function (ClusterVolumeSpecAccessModeScopeEnum) {\n    ClusterVolumeSpecAccessModeScopeEnum[\"Empty\"] = \"\";\n    ClusterVolumeSpecAccessModeScopeEnum[\"Single\"] = \"single\";\n    ClusterVolumeSpecAccessModeScopeEnum[\"Multi\"] = \"multi\";\n})(ClusterVolumeSpecAccessModeScopeEnum || (ClusterVolumeSpecAccessModeScopeEnum = {}));\nexport var ClusterVolumeSpecAccessModeSharingEnum;\n(function (ClusterVolumeSpecAccessModeSharingEnum) {\n    ClusterVolumeSpecAccessModeSharingEnum[\"Empty\"] = \"\";\n    ClusterVolumeSpecAccessModeSharingEnum[\"None\"] = \"none\";\n    ClusterVolumeSpecAccessModeSharingEnum[\"Readonly\"] = \"readonly\";\n    ClusterVolumeSpecAccessModeSharingEnum[\"Onewriter\"] = \"onewriter\";\n    ClusterVolumeSpecAccessModeSharingEnum[\"All\"] = \"all\";\n})(ClusterVolumeSpecAccessModeSharingEnum || (ClusterVolumeSpecAccessModeSharingEnum = {}));\nexport var ClusterVolumeSpecAccessModeAvailabilityEnum;\n(function (ClusterVolumeSpecAccessModeAvailabilityEnum) {\n    ClusterVolumeSpecAccessModeAvailabilityEnum[\"Empty\"] = \"\";\n    ClusterVolumeSpecAccessModeAvailabilityEnum[\"Active\"] = \"active\";\n    ClusterVolumeSpecAccessModeAvailabilityEnum[\"Pause\"] = \"pause\";\n    ClusterVolumeSpecAccessModeAvailabilityEnum[\"Drain\"] = \"drain\";\n})(ClusterVolumeSpecAccessModeAvailabilityEnum || (ClusterVolumeSpecAccessModeAvailabilityEnum = {}));\nexport var ClusterVolumePublishStatusStateEnum;\n(function (ClusterVolumePublishStatusStateEnum) {\n    ClusterVolumePublishStatusStateEnum[\"Empty\"] = \"\";\n    ClusterVolumePublishStatusStateEnum[\"PendingPublish\"] = \"pending-publish\";\n    ClusterVolumePublishStatusStateEnum[\"Published\"] = \"published\";\n    ClusterVolumePublishStatusStateEnum[\"PendingNodeUnpublish\"] = \"pending-node-unpublish\";\n    ClusterVolumePublishStatusStateEnum[\"PendingControllerUnpublish\"] = \"pending-controller-unpublish\";\n})(ClusterVolumePublishStatusStateEnum || (ClusterVolumePublishStatusStateEnum = {}));\nexport var PortTypeEnum;\n(function (PortTypeEnum) {\n    PortTypeEnum[\"EMPTY\"] = \"\";\n    PortTypeEnum[\"TCP\"] = \"tcp\";\n    PortTypeEnum[\"UDP\"] = \"udp\";\n    PortTypeEnum[\"SCTP\"] = \"sctp\";\n})(PortTypeEnum || (PortTypeEnum = {}));\nexport var ProcedureState;\n(function (ProcedureState) {\n    /** Currently running */\n    ProcedureState[\"Running\"] = \"Running\";\n    /** Last run successful */\n    ProcedureState[\"Ok\"] = \"Ok\";\n    /** Last run failed */\n    ProcedureState[\"Failed\"] = \"Failed\";\n    /** Other case (never run) */\n    ProcedureState[\"Unknown\"] = \"Unknown\";\n})(ProcedureState || (ProcedureState = {}));\nexport var RepoState;\n(function (RepoState) {\n    /** Unknown case */\n    RepoState[\"Unknown\"] = \"Unknown\";\n    /** Last clone / pull successful (or never cloned) */\n    RepoState[\"Ok\"] = \"Ok\";\n    /** Last clone / pull failed */\n    RepoState[\"Failed\"] = \"Failed\";\n    /** Currently cloning */\n    RepoState[\"Cloning\"] = \"Cloning\";\n    /** Currently pulling */\n    RepoState[\"Pulling\"] = \"Pulling\";\n    /** Currently building */\n    RepoState[\"Building\"] = \"Building\";\n})(RepoState || (RepoState = {}));\nexport var ResourceSyncState;\n(function (ResourceSyncState) {\n    /** Currently syncing */\n    ResourceSyncState[\"Syncing\"] = \"Syncing\";\n    /** Updates pending */\n    ResourceSyncState[\"Pending\"] = \"Pending\";\n    /** Last sync successful (or never synced). No Changes pending */\n    ResourceSyncState[\"Ok\"] = \"Ok\";\n    /** Last sync failed */\n    ResourceSyncState[\"Failed\"] = \"Failed\";\n    /** Other case */\n    ResourceSyncState[\"Unknown\"] = \"Unknown\";\n})(ResourceSyncState || (ResourceSyncState = {}));\nexport var ServerState;\n(function (ServerState) {\n    /** Server health check passing. */\n    ServerState[\"Ok\"] = \"Ok\";\n    /** Server is unreachable. */\n    ServerState[\"NotOk\"] = \"NotOk\";\n    /** Server is disabled. */\n    ServerState[\"Disabled\"] = \"Disabled\";\n})(ServerState || (ServerState = {}));\nexport var StackState;\n(function (StackState) {\n    /** The stack is currently re/deploying */\n    StackState[\"Deploying\"] = \"deploying\";\n    /** All containers are running. */\n    StackState[\"Running\"] = \"running\";\n    /** All containers are paused */\n    StackState[\"Paused\"] = \"paused\";\n    /** All contianers are stopped */\n    StackState[\"Stopped\"] = \"stopped\";\n    /** All containers are created */\n    StackState[\"Created\"] = \"created\";\n    /** All containers are restarting */\n    StackState[\"Restarting\"] = \"restarting\";\n    /** All containers are dead */\n    StackState[\"Dead\"] = \"dead\";\n    /** All containers are removing */\n    StackState[\"Removing\"] = \"removing\";\n    /** The containers are in a mix of states */\n    StackState[\"Unhealthy\"] = \"unhealthy\";\n    /** The stack is not deployed */\n    StackState[\"Down\"] = \"down\";\n    /** Server not reachable for status */\n    StackState[\"Unknown\"] = \"unknown\";\n})(StackState || (StackState = {}));\nexport var RepoWebhookAction;\n(function (RepoWebhookAction) {\n    RepoWebhookAction[\"Clone\"] = \"Clone\";\n    RepoWebhookAction[\"Pull\"] = \"Pull\";\n    RepoWebhookAction[\"Build\"] = \"Build\";\n})(RepoWebhookAction || (RepoWebhookAction = {}));\nexport var StackWebhookAction;\n(function (StackWebhookAction) {\n    StackWebhookAction[\"Refresh\"] = \"Refresh\";\n    StackWebhookAction[\"Deploy\"] = \"Deploy\";\n})(StackWebhookAction || (StackWebhookAction = {}));\nexport var SyncWebhookAction;\n(function (SyncWebhookAction) {\n    SyncWebhookAction[\"Refresh\"] = \"Refresh\";\n    SyncWebhookAction[\"Sync\"] = \"Sync\";\n})(SyncWebhookAction || (SyncWebhookAction = {}));\n/**\n * Configures the behavior of [CreateTerminal] if the\n * specified terminal name already exists.\n */\nexport var TerminalRecreateMode;\n(function (TerminalRecreateMode) {\n    /**\n     * Never kill the old terminal if it already exists.\n     * If the command is different, returns error.\n     */\n    TerminalRecreateMode[\"Never\"] = \"Never\";\n    /** Always kill the old terminal and create new one */\n    TerminalRecreateMode[\"Always\"] = \"Always\";\n    /** Only kill and recreate if the command is different. */\n    TerminalRecreateMode[\"DifferentCommand\"] = \"DifferentCommand\";\n})(TerminalRecreateMode || (TerminalRecreateMode = {}));\nexport var DefaultRepoFolder;\n(function (DefaultRepoFolder) {\n    /** /${root_directory}/stacks */\n    DefaultRepoFolder[\"Stacks\"] = \"Stacks\";\n    /** /${root_directory}/builds */\n    DefaultRepoFolder[\"Builds\"] = \"Builds\";\n    /** /${root_directory}/repos */\n    DefaultRepoFolder[\"Repos\"] = \"Repos\";\n    /**\n     * If the repo is only cloned\n     * in the core repo cache (resource sync),\n     * this isn't relevant.\n     */\n    DefaultRepoFolder[\"NotApplicable\"] = \"NotApplicable\";\n})(DefaultRepoFolder || (DefaultRepoFolder = {}));\nexport var SearchCombinator;\n(function (SearchCombinator) {\n    SearchCombinator[\"Or\"] = \"Or\";\n    SearchCombinator[\"And\"] = \"And\";\n})(SearchCombinator || (SearchCombinator = {}));\n/** Days of the week */\nexport var DayOfWeek;\n(function (DayOfWeek) {\n    DayOfWeek[\"Monday\"] = \"Monday\";\n    DayOfWeek[\"Tuesday\"] = \"Tuesday\";\n    DayOfWeek[\"Wednesday\"] = \"Wednesday\";\n    DayOfWeek[\"Thursday\"] = \"Thursday\";\n    DayOfWeek[\"Friday\"] = \"Friday\";\n    DayOfWeek[\"Saturday\"] = \"Saturday\";\n    DayOfWeek[\"Sunday\"] = \"Sunday\";\n})(DayOfWeek || (DayOfWeek = {}));\n/**\n * One representative IANA zone for each distinct base UTC offset in the tz database.\n * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.\n *\n * The `serde`/`strum` renames ensure the canonical identifier is used\n * when serializing or parsing from a string such as `\"Etc/UTC\"`.\n */\nexport var IanaTimezone;\n(function (IanaTimezone) {\n    /** UTC−12:00 */\n    IanaTimezone[\"EtcGmtMinus12\"] = \"Etc/GMT+12\";\n    /** UTC−11:00 */\n    IanaTimezone[\"PacificPagoPago\"] = \"Pacific/Pago_Pago\";\n    /** UTC−10:00 */\n    IanaTimezone[\"PacificHonolulu\"] = \"Pacific/Honolulu\";\n    /** UTC−09:30 */\n    IanaTimezone[\"PacificMarquesas\"] = \"Pacific/Marquesas\";\n    /** UTC−09:00 */\n    IanaTimezone[\"AmericaAnchorage\"] = \"America/Anchorage\";\n    /** UTC−08:00 */\n    IanaTimezone[\"AmericaLosAngeles\"] = \"America/Los_Angeles\";\n    /** UTC−07:00 */\n    IanaTimezone[\"AmericaDenver\"] = \"America/Denver\";\n    /** UTC−06:00 */\n    IanaTimezone[\"AmericaChicago\"] = \"America/Chicago\";\n    /** UTC−05:00 */\n    IanaTimezone[\"AmericaNewYork\"] = \"America/New_York\";\n    /** UTC−04:00 */\n    IanaTimezone[\"AmericaHalifax\"] = \"America/Halifax\";\n    /** UTC−03:30 */\n    IanaTimezone[\"AmericaStJohns\"] = \"America/St_Johns\";\n    /** UTC−03:00 */\n    IanaTimezone[\"AmericaSaoPaulo\"] = \"America/Sao_Paulo\";\n    /** UTC−02:00 */\n    IanaTimezone[\"AmericaNoronha\"] = \"America/Noronha\";\n    /** UTC−01:00 */\n    IanaTimezone[\"AtlanticAzores\"] = \"Atlantic/Azores\";\n    /** UTC±00:00 */\n    IanaTimezone[\"EtcUtc\"] = \"Etc/UTC\";\n    /** UTC+01:00 */\n    IanaTimezone[\"EuropeBerlin\"] = \"Europe/Berlin\";\n    /** UTC+02:00 */\n    IanaTimezone[\"EuropeBucharest\"] = \"Europe/Bucharest\";\n    /** UTC+03:00 */\n    IanaTimezone[\"EuropeMoscow\"] = \"Europe/Moscow\";\n    /** UTC+03:30 */\n    IanaTimezone[\"AsiaTehran\"] = \"Asia/Tehran\";\n    /** UTC+04:00 */\n    IanaTimezone[\"AsiaDubai\"] = \"Asia/Dubai\";\n    /** UTC+04:30 */\n    IanaTimezone[\"AsiaKabul\"] = \"Asia/Kabul\";\n    /** UTC+05:00 */\n    IanaTimezone[\"AsiaKarachi\"] = \"Asia/Karachi\";\n    /** UTC+05:30 */\n    IanaTimezone[\"AsiaKolkata\"] = \"Asia/Kolkata\";\n    /** UTC+05:45 */\n    IanaTimezone[\"AsiaKathmandu\"] = \"Asia/Kathmandu\";\n    /** UTC+06:00 */\n    IanaTimezone[\"AsiaDhaka\"] = \"Asia/Dhaka\";\n    /** UTC+06:30 */\n    IanaTimezone[\"AsiaYangon\"] = \"Asia/Yangon\";\n    /** UTC+07:00 */\n    IanaTimezone[\"AsiaBangkok\"] = \"Asia/Bangkok\";\n    /** UTC+08:00 */\n    IanaTimezone[\"AsiaShanghai\"] = \"Asia/Shanghai\";\n    /** UTC+08:45 */\n    IanaTimezone[\"AustraliaEucla\"] = \"Australia/Eucla\";\n    /** UTC+09:00 */\n    IanaTimezone[\"AsiaTokyo\"] = \"Asia/Tokyo\";\n    /** UTC+09:30 */\n    IanaTimezone[\"AustraliaAdelaide\"] = \"Australia/Adelaide\";\n    /** UTC+10:00 */\n    IanaTimezone[\"AustraliaSydney\"] = \"Australia/Sydney\";\n    /** UTC+10:30 */\n    IanaTimezone[\"AustraliaLordHowe\"] = \"Australia/Lord_Howe\";\n    /** UTC+11:00 */\n    IanaTimezone[\"PacificPortMoresby\"] = \"Pacific/Port_Moresby\";\n    /** UTC+12:00 */\n    IanaTimezone[\"PacificAuckland\"] = \"Pacific/Auckland\";\n    /** UTC+12:45 */\n    IanaTimezone[\"PacificChatham\"] = \"Pacific/Chatham\";\n    /** UTC+13:00 */\n    IanaTimezone[\"PacificTongatapu\"] = \"Pacific/Tongatapu\";\n    /** UTC+14:00 */\n    IanaTimezone[\"PacificKiritimati\"] = \"Pacific/Kiritimati\";\n})(IanaTimezone || (IanaTimezone = {}));\n/** The specific types of permission that a User or UserGroup can have on a resource. */\nexport var SpecificPermission;\n(function (SpecificPermission) {\n    /**\n     * On **Server**\n     * - Access the terminal apis\n     * On **Stack / Deployment**\n     * - Access the container exec Apis\n     */\n    SpecificPermission[\"Terminal\"] = \"Terminal\";\n    /**\n     * On **Server**\n     * - Allowed to attach Stacks, Deployments, Repos, Builders to the Server\n     * On **Builder**\n     * - Allowed to attach Builds to the Builder\n     * On **Build**\n     * - Allowed to attach Deployments to the Build\n     */\n    SpecificPermission[\"Attach\"] = \"Attach\";\n    /**\n     * On **Server**\n     * - Access the `container inspect` apis\n     * On **Stack / Deployment**\n     * - Access `container inspect` apis for associated containers\n     */\n    SpecificPermission[\"Inspect\"] = \"Inspect\";\n    /**\n     * On **Server**\n     * - Read all container logs on the server\n     * On **Stack / Deployment**\n     * - Read the container logs\n     */\n    SpecificPermission[\"Logs\"] = \"Logs\";\n    /**\n     * On **Server**\n     * - Read all the processes on the host\n     */\n    SpecificPermission[\"Processes\"] = \"Processes\";\n})(SpecificPermission || (SpecificPermission = {}));\n"
  },
  {
    "path": "frontend/public/deno.d.ts",
    "content": "// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.\n\n/** Deno provides extra properties on `import.meta`. These are included here\n * to ensure that these are still available when using the Deno namespace in\n * conjunction with other type libs, like `dom`.\n *\n * @category Platform\n */\ninterface ImportMeta {\n  /** A string representation of the fully qualified module URL. When the\n   * module is loaded locally, the value will be a file URL (e.g.\n   * `file:///path/module.ts`).\n   *\n   * You can also parse the string as a URL to determine more information about\n   * how the current module was loaded. For example to determine if a module was\n   * local or not:\n   *\n   * ```ts\n   * const url = new URL(import.meta.url);\n   * if (url.protocol === \"file:\") {\n   *   console.log(\"this module was loaded locally\");\n   * }\n   * ```\n   */\n  url: string;\n\n  /** The absolute path of the current module.\n   *\n   * This property is only provided for local modules (ie. using `file://` URLs).\n   *\n   * Example:\n   * ```\n   * // Unix\n   * console.log(import.meta.filename); // /home/alice/my_module.ts\n   *\n   * // Windows\n   * console.log(import.meta.filename); // C:\\alice\\my_module.ts\n   * ```\n   */\n  filename?: string;\n\n  /** The absolute path of the directory containing the current module.\n   *\n   * This property is only provided for local modules (ie. using `file://` URLs).\n   *\n   * * Example:\n   * ```\n   * // Unix\n   * console.log(import.meta.dirname); // /home/alice\n   *\n   * // Windows\n   * console.log(import.meta.dirname); // C:\\alice\n   * ```\n   */\n  dirname?: string;\n\n  /** A flag that indicates if the current module is the main module that was\n   * called when starting the program under Deno.\n   *\n   * ```ts\n   * if (import.meta.main) {\n   *   // this was loaded as the main module, maybe do some bootstrapping\n   * }\n   * ```\n   */\n  main: boolean;\n\n  /** A function that returns resolved specifier as if it would be imported\n   * using `import(specifier)`.\n   *\n   * ```ts\n   * console.log(import.meta.resolve(\"./foo.js\"));\n   * // file:///dev/foo.js\n   * ```\n   */\n  resolve(specifier: string): string;\n}\n\n/** Deno supports [User Timing Level 3](https://w3c.github.io/user-timing)\n * which is not widely supported yet in other runtimes.\n *\n * Check out the\n * [Performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance)\n * documentation on MDN for further information about how to use the API.\n *\n * @category Performance\n */\ninterface Performance {\n  /** Stores a timestamp with the associated name (a \"mark\"). */\n  mark(markName: string, options?: PerformanceMarkOptions): PerformanceMark;\n\n  /** Stores the `DOMHighResTimeStamp` duration between two marks along with the\n   * associated name (a \"measure\"). */\n  measure(\n    measureName: string,\n    options?: PerformanceMeasureOptions\n  ): PerformanceMeasure;\n}\n\n/**\n * Options which are used in conjunction with `performance.mark`. Check out the\n * MDN\n * [`performance.mark()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark#markoptions)\n * documentation for more details.\n *\n * @category Performance\n */\ninterface PerformanceMarkOptions {\n  /** Metadata to be included in the mark. */\n  // deno-lint-ignore no-explicit-any\n  detail?: any;\n\n  /** Timestamp to be used as the mark time. */\n  startTime?: number;\n}\n\n/**\n * Options which are used in conjunction with `performance.measure`. Check out the\n * MDN\n * [`performance.mark()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure#measureoptions)\n * documentation for more details.\n *\n * @category Performance\n */\ninterface PerformanceMeasureOptions {\n  /** Metadata to be included in the measure. */\n  // deno-lint-ignore no-explicit-any\n  detail?: any;\n\n  /** Timestamp to be used as the start time or string to be used as start\n   * mark. */\n  start?: string | number;\n\n  /** Duration between the start and end times. */\n  duration?: number;\n\n  /** Timestamp to be used as the end time or string to be used as end mark. */\n  end?: string | number;\n}\n\n/** The global namespace where Deno specific, non-standard APIs are located. */\ndeclare namespace Deno {\n  /** A set of error constructors that are raised by Deno APIs.\n   *\n   * Can be used to provide more specific handling of failures within code\n   * which is using Deno APIs. For example, handling attempting to open a file\n   * which does not exist:\n   *\n   * ```ts\n   * try {\n   *   const file = await Deno.open(\"./some/file.txt\");\n   * } catch (error) {\n   *   if (error instanceof Deno.errors.NotFound) {\n   *     console.error(\"the file was not found\");\n   *   } else {\n   *     // otherwise re-throw\n   *     throw error;\n   *   }\n   * }\n   * ```\n   *\n   * @category Errors\n   */\n  export namespace errors {\n    /**\n     * Raised when the underlying operating system indicates that the file\n     * was not found.\n     *\n     * @category Errors */\n    export class NotFound extends Error {}\n    /**\n     * Raised when the underlying operating system indicates the current user\n     * which the Deno process is running under does not have the appropriate\n     * permissions to a file or resource.\n     *\n     * Before Deno 2.0, this error was raised when the user _did not_ provide\n     * required `--allow-*` flag. As of Deno 2.0, that case is now handled by\n     * the {@link NotCapable} error.\n     *\n     * @category Errors */\n    export class PermissionDenied extends Error {}\n    /**\n     * Raised when the underlying operating system reports that a connection to\n     * a resource is refused.\n     *\n     * @category Errors */\n    export class ConnectionRefused extends Error {}\n    /**\n     * Raised when the underlying operating system reports that a connection has\n     * been reset. With network servers, it can be a _normal_ occurrence where a\n     * client will abort a connection instead of properly shutting it down.\n     *\n     * @category Errors */\n    export class ConnectionReset extends Error {}\n    /**\n     * Raised when the underlying operating system reports an `ECONNABORTED`\n     * error.\n     *\n     * @category Errors */\n    export class ConnectionAborted extends Error {}\n    /**\n     * Raised when the underlying operating system reports an `ENOTCONN` error.\n     *\n     * @category Errors */\n    export class NotConnected extends Error {}\n    /**\n     * Raised when attempting to open a server listener on an address and port\n     * that already has a listener.\n     *\n     * @category Errors */\n    export class AddrInUse extends Error {}\n    /**\n     * Raised when the underlying operating system reports an `EADDRNOTAVAIL`\n     * error.\n     *\n     * @category Errors */\n    export class AddrNotAvailable extends Error {}\n    /**\n     * Raised when trying to write to a resource and a broken pipe error occurs.\n     * This can happen when trying to write directly to `stdout` or `stderr`\n     * and the operating system is unable to pipe the output for a reason\n     * external to the Deno runtime.\n     *\n     * @category Errors */\n    export class BrokenPipe extends Error {}\n    /**\n     * Raised when trying to create a resource, like a file, that already\n     * exits.\n     *\n     * @category Errors */\n    export class AlreadyExists extends Error {}\n    /**\n     * Raised when an operation to returns data that is invalid for the\n     * operation being performed.\n     *\n     * @category Errors */\n    export class InvalidData extends Error {}\n    /**\n     * Raised when the underlying operating system reports that an I/O operation\n     * has timed out (`ETIMEDOUT`).\n     *\n     * @category Errors */\n    export class TimedOut extends Error {}\n    /**\n     * Raised when the underlying operating system reports an `EINTR` error. In\n     * many cases, this underlying IO error will be handled internally within\n     * Deno, or result in an @{link BadResource} error instead.\n     *\n     * @category Errors */\n    export class Interrupted extends Error {}\n    /**\n     * Raised when the underlying operating system would need to block to\n     * complete but an asynchronous (non-blocking) API is used.\n     *\n     * @category Errors */\n    export class WouldBlock extends Error {}\n    /**\n     * Raised when expecting to write to a IO buffer resulted in zero bytes\n     * being written.\n     *\n     * @category Errors */\n    export class WriteZero extends Error {}\n    /**\n     * Raised when attempting to read bytes from a resource, but the EOF was\n     * unexpectedly encountered.\n     *\n     * @category Errors */\n    export class UnexpectedEof extends Error {}\n    /**\n     * The underlying IO resource is invalid or closed, and so the operation\n     * could not be performed.\n     *\n     * @category Errors */\n    export class BadResource extends Error {}\n    /**\n     * Raised in situations where when attempting to load a dynamic import,\n     * too many redirects were encountered.\n     *\n     * @category Errors */\n    export class Http extends Error {}\n    /**\n     * Raised when the underlying IO resource is not available because it is\n     * being awaited on in another block of code.\n     *\n     * @category Errors */\n    export class Busy extends Error {}\n    /**\n     * Raised when the underlying Deno API is asked to perform a function that\n     * is not currently supported.\n     *\n     * @category Errors */\n    export class NotSupported extends Error {}\n    /**\n     * Raised when too many symbolic links were encountered when resolving the\n     * filename.\n     *\n     * @category Errors */\n    export class FilesystemLoop extends Error {}\n    /**\n     * Raised when trying to open, create or write to a directory.\n     *\n     * @category Errors */\n    export class IsADirectory extends Error {}\n    /**\n     * Raised when performing a socket operation but the remote host is\n     * not reachable.\n     *\n     * @category Errors */\n    export class NetworkUnreachable extends Error {}\n    /**\n     * Raised when trying to perform an operation on a path that is not a\n     * directory, when directory is required.\n     *\n     * @category Errors */\n    export class NotADirectory extends Error {}\n\n    /**\n     * Raised when trying to perform an operation while the relevant Deno\n     * permission (like `--allow-read`) has not been granted.\n     *\n     * Before Deno 2.0, this condition was covered by the {@link PermissionDenied}\n     * error.\n     *\n     * @category Errors */\n    export class NotCapable extends Error {}\n\n    export {}; // only export exports\n  }\n\n  /** The current process ID of this instance of the Deno CLI.\n   *\n   * ```ts\n   * console.log(Deno.pid);\n   * ```\n   *\n   * @category Runtime\n   */\n  export const pid: number;\n\n  /**\n   * The process ID of parent process of this instance of the Deno CLI.\n   *\n   * ```ts\n   * console.log(Deno.ppid);\n   * ```\n   *\n   * @category Runtime\n   */\n  export const ppid: number;\n\n  /** @category Runtime */\n  export interface MemoryUsage {\n    /** The number of bytes of the current Deno's process resident set size,\n     * which is the amount of memory occupied in main memory (RAM). */\n    rss: number;\n    /** The total size of the heap for V8, in bytes. */\n    heapTotal: number;\n    /** The amount of the heap used for V8, in bytes. */\n    heapUsed: number;\n    /** Memory, in bytes, associated with JavaScript objects outside of the\n     * JavaScript isolate. */\n    external: number;\n  }\n\n  /**\n   * Returns an object describing the memory usage of the Deno process and the\n   * V8 subsystem measured in bytes.\n   *\n   * @category Runtime\n   */\n  export function memoryUsage(): MemoryUsage;\n\n  /**\n   * Get the `hostname` of the machine the Deno process is running on.\n   *\n   * ```ts\n   * console.log(Deno.hostname());\n   * ```\n   *\n   * Requires `allow-sys` permission.\n   *\n   * @tags allow-sys\n   * @category Runtime\n   */\n  export function hostname(): string;\n\n  /**\n   * Returns an array containing the 1, 5, and 15 minute load averages. The\n   * load average is a measure of CPU and IO utilization of the last one, five,\n   * and 15 minute periods expressed as a fractional number.  Zero means there\n   * is no load. On Windows, the three values are always the same and represent\n   * the current load, not the 1, 5 and 15 minute load averages.\n   *\n   * ```ts\n   * console.log(Deno.loadavg());  // e.g. [ 0.71, 0.44, 0.44 ]\n   * ```\n   *\n   * Requires `allow-sys` permission.\n   *\n   * On Windows there is no API available to retrieve this information and this method returns `[ 0, 0, 0 ]`.\n   *\n   * @tags allow-sys\n   * @category Runtime\n   */\n  export function loadavg(): number[];\n\n  /**\n   * The information for a network interface returned from a call to\n   * {@linkcode Deno.networkInterfaces}.\n   *\n   * @category Network\n   */\n  export interface NetworkInterfaceInfo {\n    /** The network interface name. */\n    name: string;\n    /** The IP protocol version. */\n    family: \"IPv4\" | \"IPv6\";\n    /** The IP address bound to the interface. */\n    address: string;\n    /** The netmask applied to the interface. */\n    netmask: string;\n    /** The IPv6 scope id or `null`. */\n    scopeid: number | null;\n    /** The CIDR range. */\n    cidr: string;\n    /** The MAC address. */\n    mac: string;\n  }\n\n  /**\n   * Returns an array of the network interface information.\n   *\n   * ```ts\n   * console.log(Deno.networkInterfaces());\n   * ```\n   *\n   * Requires `allow-sys` permission.\n   *\n   * @tags allow-sys\n   * @category Network\n   */\n  export function networkInterfaces(): NetworkInterfaceInfo[];\n\n  /**\n   * Displays the total amount of free and used physical and swap memory in the\n   * system, as well as the buffers and caches used by the kernel.\n   *\n   * This is similar to the `free` command in Linux\n   *\n   * ```ts\n   * console.log(Deno.systemMemoryInfo());\n   * ```\n   *\n   * Requires `allow-sys` permission.\n   *\n   * @tags allow-sys\n   * @category Runtime\n   */\n  export function systemMemoryInfo(): SystemMemoryInfo;\n\n  /**\n   * Information returned from a call to {@linkcode Deno.systemMemoryInfo}.\n   *\n   * @category Runtime\n   */\n  export interface SystemMemoryInfo {\n    /** Total installed memory in bytes. */\n    total: number;\n    /** Unused memory in bytes. */\n    free: number;\n    /** Estimation of how much memory, in bytes, is available for starting new\n     * applications, without swapping. Unlike the data provided by the cache or\n     * free fields, this field takes into account page cache and also that not\n     * all reclaimable memory will be reclaimed due to items being in use.\n     */\n    available: number;\n    /** Memory used by kernel buffers. */\n    buffers: number;\n    /** Memory used by the page cache and slabs. */\n    cached: number;\n    /** Total swap memory. */\n    swapTotal: number;\n    /** Unused swap memory. */\n    swapFree: number;\n  }\n\n  /** Reflects the `NO_COLOR` environment variable at program start.\n   *\n   * When the value is `true`, the Deno CLI will attempt to not send color codes\n   * to `stderr` or `stdout` and other command line programs should also attempt\n   * to respect this value.\n   *\n   * See: https://no-color.org/\n   *\n   * @category Runtime\n   */\n  export const noColor: boolean;\n\n  /**\n   * Returns the release version of the Operating System.\n   *\n   * ```ts\n   * console.log(Deno.osRelease());\n   * ```\n   *\n   * Requires `allow-sys` permission.\n   * Under consideration to possibly move to Deno.build or Deno.versions and if\n   * it should depend sys-info, which may not be desirable.\n   *\n   * @tags allow-sys\n   * @category Runtime\n   */\n  export function osRelease(): string;\n\n  /**\n   * Returns the Operating System uptime in number of seconds.\n   *\n   * ```ts\n   * console.log(Deno.osUptime());\n   * ```\n   *\n   * Requires `allow-sys` permission.\n   *\n   * @tags allow-sys\n   * @category Runtime\n   */\n  export function osUptime(): number;\n\n  /**\n   * Options which define the permissions within a test or worker context.\n   *\n   * `\"inherit\"` ensures that all permissions of the parent process will be\n   * applied to the test context. `\"none\"` ensures the test context has no\n   * permissions. A `PermissionOptionsObject` provides a more specific\n   * set of permissions to the test context.\n   *\n   * @category Permissions */\n  export type PermissionOptions = \"inherit\" | \"none\" | PermissionOptionsObject;\n\n  /**\n   * A set of options which can define the permissions within a test or worker\n   * context at a highly specific level.\n   *\n   * @category Permissions */\n  export interface PermissionOptionsObject {\n    /** Specifies if the `env` permission should be requested or revoked.\n     * If set to `\"inherit\"`, the current `env` permission will be inherited.\n     * If set to `true`, the global `env` permission will be requested.\n     * If set to `false`, the global `env` permission will be revoked.\n     *\n     * @default {false}\n     */\n    env?: \"inherit\" | boolean | string[];\n\n    /** Specifies if the `sys` permission should be requested or revoked.\n     * If set to `\"inherit\"`, the current `sys` permission will be inherited.\n     * If set to `true`, the global `sys` permission will be requested.\n     * If set to `false`, the global `sys` permission will be revoked.\n     *\n     * @default {false}\n     */\n    sys?: \"inherit\" | boolean | string[];\n\n    /** Specifies if the `net` permission should be requested or revoked.\n     * if set to `\"inherit\"`, the current `net` permission will be inherited.\n     * if set to `true`, the global `net` permission will be requested.\n     * if set to `false`, the global `net` permission will be revoked.\n     * if set to `string[]`, the `net` permission will be requested with the\n     * specified host strings with the format `\"<host>[:<port>]`.\n     *\n     * @default {false}\n     *\n     * Examples:\n     *\n     * ```ts\n     * import { assertEquals } from \"jsr:@std/assert\";\n     *\n     * Deno.test({\n     *   name: \"inherit\",\n     *   permissions: {\n     *     net: \"inherit\",\n     *   },\n     *   async fn() {\n     *     const status = await Deno.permissions.query({ name: \"net\" })\n     *     assertEquals(status.state, \"granted\");\n     *   },\n     * });\n     * ```\n     *\n     * ```ts\n     * import { assertEquals } from \"jsr:@std/assert\";\n     *\n     * Deno.test({\n     *   name: \"true\",\n     *   permissions: {\n     *     net: true,\n     *   },\n     *   async fn() {\n     *     const status = await Deno.permissions.query({ name: \"net\" });\n     *     assertEquals(status.state, \"granted\");\n     *   },\n     * });\n     * ```\n     *\n     * ```ts\n     * import { assertEquals } from \"jsr:@std/assert\";\n     *\n     * Deno.test({\n     *   name: \"false\",\n     *   permissions: {\n     *     net: false,\n     *   },\n     *   async fn() {\n     *     const status = await Deno.permissions.query({ name: \"net\" });\n     *     assertEquals(status.state, \"denied\");\n     *   },\n     * });\n     * ```\n     *\n     * ```ts\n     * import { assertEquals } from \"jsr:@std/assert\";\n     *\n     * Deno.test({\n     *   name: \"localhost:8080\",\n     *   permissions: {\n     *     net: [\"localhost:8080\"],\n     *   },\n     *   async fn() {\n     *     const status = await Deno.permissions.query({ name: \"net\", host: \"localhost:8080\" });\n     *     assertEquals(status.state, \"granted\");\n     *   },\n     * });\n     * ```\n     */\n    net?: \"inherit\" | boolean | string[];\n\n    /** Specifies if the `ffi` permission should be requested or revoked.\n     * If set to `\"inherit\"`, the current `ffi` permission will be inherited.\n     * If set to `true`, the global `ffi` permission will be requested.\n     * If set to `false`, the global `ffi` permission will be revoked.\n     *\n     * @default {false}\n     */\n    ffi?: \"inherit\" | boolean | Array<string | URL>;\n\n    /** Specifies if the `read` permission should be requested or revoked.\n     * If set to `\"inherit\"`, the current `read` permission will be inherited.\n     * If set to `true`, the global `read` permission will be requested.\n     * If set to `false`, the global `read` permission will be revoked.\n     * If set to `Array<string | URL>`, the `read` permission will be requested with the\n     * specified file paths.\n     *\n     * @default {false}\n     */\n    read?: \"inherit\" | boolean | Array<string | URL>;\n\n    /** Specifies if the `run` permission should be requested or revoked.\n     * If set to `\"inherit\"`, the current `run` permission will be inherited.\n     * If set to `true`, the global `run` permission will be requested.\n     * If set to `false`, the global `run` permission will be revoked.\n     *\n     * @default {false}\n     */\n    run?: \"inherit\" | boolean | Array<string | URL>;\n\n    /** Specifies if the `write` permission should be requested or revoked.\n     * If set to `\"inherit\"`, the current `write` permission will be inherited.\n     * If set to `true`, the global `write` permission will be requested.\n     * If set to `false`, the global `write` permission will be revoked.\n     * If set to `Array<string | URL>`, the `write` permission will be requested with the\n     * specified file paths.\n     *\n     * @default {false}\n     */\n    write?: \"inherit\" | boolean | Array<string | URL>;\n  }\n\n  /**\n   * Context that is passed to a testing function, which can be used to either\n   * gain information about the current test, or register additional test\n   * steps within the current test.\n   *\n   * @category Testing */\n  export interface TestContext {\n    /** The current test name. */\n    name: string;\n    /** The string URL of the current test. */\n    origin: string;\n    /** If the current test is a step of another test, the parent test context\n     * will be set here. */\n    parent?: TestContext;\n\n    /** Run a sub step of the parent test or step. Returns a promise\n     * that resolves to a boolean signifying if the step completed successfully.\n     *\n     * The returned promise never rejects unless the arguments are invalid.\n     *\n     * If the test was ignored the promise returns `false`.\n     *\n     * ```ts\n     * Deno.test({\n     *   name: \"a parent test\",\n     *   async fn(t) {\n     *     console.log(\"before the step\");\n     *     await t.step({\n     *       name: \"step 1\",\n     *       fn(t) {\n     *         console.log(\"current step:\", t.name);\n     *       }\n     *     });\n     *     console.log(\"after the step\");\n     *   }\n     * });\n     * ```\n     */\n    step(definition: TestStepDefinition): Promise<boolean>;\n\n    /** Run a sub step of the parent test or step. Returns a promise\n     * that resolves to a boolean signifying if the step completed successfully.\n     *\n     * The returned promise never rejects unless the arguments are invalid.\n     *\n     * If the test was ignored the promise returns `false`.\n     *\n     * ```ts\n     * Deno.test(\n     *   \"a parent test\",\n     *   async (t) => {\n     *     console.log(\"before the step\");\n     *     await t.step(\n     *       \"step 1\",\n     *       (t) => {\n     *         console.log(\"current step:\", t.name);\n     *       }\n     *     );\n     *     console.log(\"after the step\");\n     *   }\n     * );\n     * ```\n     */\n    step(\n      name: string,\n      fn: (t: TestContext) => void | Promise<void>\n    ): Promise<boolean>;\n\n    /** Run a sub step of the parent test or step. Returns a promise\n     * that resolves to a boolean signifying if the step completed successfully.\n     *\n     * The returned promise never rejects unless the arguments are invalid.\n     *\n     * If the test was ignored the promise returns `false`.\n     *\n     * ```ts\n     * Deno.test(async function aParentTest(t) {\n     *   console.log(\"before the step\");\n     *   await t.step(function step1(t) {\n     *     console.log(\"current step:\", t.name);\n     *   });\n     *   console.log(\"after the step\");\n     * });\n     * ```\n     */\n    step(fn: (t: TestContext) => void | Promise<void>): Promise<boolean>;\n  }\n\n  /** @category Testing */\n  export interface TestStepDefinition {\n    /** The test function that will be tested when this step is executed. The\n     * function can take an argument which will provide information about the\n     * current step's context. */\n    fn: (t: TestContext) => void | Promise<void>;\n    /** The name of the step. */\n    name: string;\n    /** If truthy the current test step will be ignored.\n     *\n     * This is a quick way to skip over a step, but also can be used for\n     * conditional logic, like determining if an environment feature is present.\n     */\n    ignore?: boolean;\n    /** Check that the number of async completed operations after the test step\n     * is the same as number of dispatched operations. This ensures that the\n     * code tested does not start async operations which it then does\n     * not await. This helps in preventing logic errors and memory leaks\n     * in the application code.\n     *\n     * Defaults to the parent test or step's value. */\n    sanitizeOps?: boolean;\n    /** Ensure the test step does not \"leak\" resources - like open files or\n     * network connections - by ensuring the open resources at the start of the\n     * step match the open resources at the end of the step.\n     *\n     * Defaults to the parent test or step's value. */\n    sanitizeResources?: boolean;\n    /** Ensure the test step does not prematurely cause the process to exit,\n     * for example via a call to {@linkcode Deno.exit}.\n     *\n     * Defaults to the parent test or step's value. */\n    sanitizeExit?: boolean;\n  }\n\n  /** @category Testing */\n  export interface TestDefinition {\n    fn: (t: TestContext) => void | Promise<void>;\n    /** The name of the test. */\n    name: string;\n    /** If truthy the current test step will be ignored.\n     *\n     * It is a quick way to skip over a step, but also can be used for\n     * conditional logic, like determining if an environment feature is present.\n     */\n    ignore?: boolean;\n    /** If at least one test has `only` set to `true`, only run tests that have\n     * `only` set to `true` and fail the test suite. */\n    only?: boolean;\n    /** Check that the number of async completed operations after the test step\n     * is the same as number of dispatched operations. This ensures that the\n     * code tested does not start async operations which it then does\n     * not await. This helps in preventing logic errors and memory leaks\n     * in the application code.\n     *\n     * @default {true} */\n    sanitizeOps?: boolean;\n    /** Ensure the test step does not \"leak\" resources - like open files or\n     * network connections - by ensuring the open resources at the start of the\n     * test match the open resources at the end of the test.\n     *\n     * @default {true} */\n    sanitizeResources?: boolean;\n    /** Ensure the test case does not prematurely cause the process to exit,\n     * for example via a call to {@linkcode Deno.exit}.\n     *\n     * @default {true} */\n    sanitizeExit?: boolean;\n    /** Specifies the permissions that should be used to run the test.\n     *\n     * Set this to \"inherit\" to keep the calling runtime permissions, set this\n     * to \"none\" to revoke all permissions, or set a more specific set of\n     * permissions using a {@linkcode PermissionOptionsObject}.\n     *\n     * @default {\"inherit\"} */\n    permissions?: PermissionOptions;\n  }\n\n  /** Register a test which will be run when `deno test` is used on the command\n   * line and the containing module looks like a test module.\n   *\n   * `fn` can be async if required.\n   *\n   * ```ts\n   * import { assertEquals } from \"jsr:@std/assert\";\n   *\n   * Deno.test({\n   *   name: \"example test\",\n   *   fn() {\n   *     assertEquals(\"world\", \"world\");\n   *   },\n   * });\n   *\n   * Deno.test({\n   *   name: \"example ignored test\",\n   *   ignore: Deno.build.os === \"windows\",\n   *   fn() {\n   *     // This test is ignored only on Windows machines\n   *   },\n   * });\n   *\n   * Deno.test({\n   *   name: \"example async test\",\n   *   async fn() {\n   *     const decoder = new TextDecoder(\"utf-8\");\n   *     const data = await Deno.readFile(\"hello_world.txt\");\n   *     assertEquals(decoder.decode(data), \"Hello world\");\n   *   }\n   * });\n   * ```\n   *\n   * @category Testing\n   */\n  export const test: DenoTest;\n\n  /**\n   * @category Testing\n   */\n  export interface DenoTest {\n    /** Register a test which will be run when `deno test` is used on the command\n     * line and the containing module looks like a test module.\n     *\n     * `fn` can be async if required.\n     *\n     * ```ts\n     * import { assertEquals } from \"jsr:@std/assert\";\n     *\n     * Deno.test({\n     *   name: \"example test\",\n     *   fn() {\n     *     assertEquals(\"world\", \"world\");\n     *   },\n     * });\n     *\n     * Deno.test({\n     *   name: \"example ignored test\",\n     *   ignore: Deno.build.os === \"windows\",\n     *   fn() {\n     *     // This test is ignored only on Windows machines\n     *   },\n     * });\n     *\n     * Deno.test({\n     *   name: \"example async test\",\n     *   async fn() {\n     *     const decoder = new TextDecoder(\"utf-8\");\n     *     const data = await Deno.readFile(\"hello_world.txt\");\n     *     assertEquals(decoder.decode(data), \"Hello world\");\n     *   }\n     * });\n     * ```\n     *\n     * @category Testing\n     */\n    (t: TestDefinition): void;\n\n    /** Register a test which will be run when `deno test` is used on the command\n     * line and the containing module looks like a test module.\n     *\n     * `fn` can be async if required.\n     *\n     * ```ts\n     * import { assertEquals } from \"jsr:@std/assert\";\n     *\n     * Deno.test(\"My test description\", () => {\n     *   assertEquals(\"hello\", \"hello\");\n     * });\n     *\n     * Deno.test(\"My async test description\", async () => {\n     *   const decoder = new TextDecoder(\"utf-8\");\n     *   const data = await Deno.readFile(\"hello_world.txt\");\n     *   assertEquals(decoder.decode(data), \"Hello world\");\n     * });\n     * ```\n     *\n     * @category Testing\n     */\n    (name: string, fn: (t: TestContext) => void | Promise<void>): void;\n\n    /** Register a test which will be run when `deno test` is used on the command\n     * line and the containing module looks like a test module.\n     *\n     * `fn` can be async if required. Declared function must have a name.\n     *\n     * ```ts\n     * import { assertEquals } from \"jsr:@std/assert\";\n     *\n     * Deno.test(function myTestName() {\n     *   assertEquals(\"hello\", \"hello\");\n     * });\n     *\n     * Deno.test(async function myOtherTestName() {\n     *   const decoder = new TextDecoder(\"utf-8\");\n     *   const data = await Deno.readFile(\"hello_world.txt\");\n     *   assertEquals(decoder.decode(data), \"Hello world\");\n     * });\n     * ```\n     *\n     * @category Testing\n     */\n    (fn: (t: TestContext) => void | Promise<void>): void;\n\n    /** Register a test which will be run when `deno test` is used on the command\n     * line and the containing module looks like a test module.\n     *\n     * `fn` can be async if required.\n     *\n     * ```ts\n     * import { assert, fail, assertEquals } from \"jsr:@std/assert\";\n     *\n     * Deno.test(\"My test description\", { permissions: { read: true } }, (): void => {\n     *   assertEquals(\"hello\", \"hello\");\n     * });\n     *\n     * Deno.test(\"My async test description\", { permissions: { read: false } }, async (): Promise<void> => {\n     *   const decoder = new TextDecoder(\"utf-8\");\n     *   const data = await Deno.readFile(\"hello_world.txt\");\n     *   assertEquals(decoder.decode(data), \"Hello world\");\n     * });\n     * ```\n     *\n     * @category Testing\n     */\n    (\n      name: string,\n      options: Omit<TestDefinition, \"fn\" | \"name\">,\n      fn: (t: TestContext) => void | Promise<void>\n    ): void;\n\n    /** Register a test which will be run when `deno test` is used on the command\n     * line and the containing module looks like a test module.\n     *\n     * `fn` can be async if required.\n     *\n     * ```ts\n     * import { assertEquals } from \"jsr:@std/assert\";\n     *\n     * Deno.test(\n     *   {\n     *     name: \"My test description\",\n     *     permissions: { read: true },\n     *   },\n     *   () => {\n     *     assertEquals(\"hello\", \"hello\");\n     *   },\n     * );\n     *\n     * Deno.test(\n     *   {\n     *     name: \"My async test description\",\n     *     permissions: { read: false },\n     *   },\n     *   async () => {\n     *     const decoder = new TextDecoder(\"utf-8\");\n     *     const data = await Deno.readFile(\"hello_world.txt\");\n     *     assertEquals(decoder.decode(data), \"Hello world\");\n     *   },\n     * );\n     * ```\n     *\n     * @category Testing\n     */\n    (\n      options: Omit<TestDefinition, \"fn\" | \"name\">,\n      fn: (t: TestContext) => void | Promise<void>\n    ): void;\n\n    /** Register a test which will be run when `deno test` is used on the command\n     * line and the containing module looks like a test module.\n     *\n     * `fn` can be async if required. Declared function must have a name.\n     *\n     * ```ts\n     * import { assertEquals } from \"jsr:@std/assert\";\n     *\n     * Deno.test(\n     *   { permissions: { read: true } },\n     *   function myTestName() {\n     *     assertEquals(\"hello\", \"hello\");\n     *   },\n     * );\n     *\n     * Deno.test(\n     *   { permissions: { read: false } },\n     *   async function myOtherTestName() {\n     *     const decoder = new TextDecoder(\"utf-8\");\n     *     const data = await Deno.readFile(\"hello_world.txt\");\n     *     assertEquals(decoder.decode(data), \"Hello world\");\n     *   },\n     * );\n     * ```\n     *\n     * @category Testing\n     */\n    (\n      options: Omit<TestDefinition, \"fn\">,\n      fn: (t: TestContext) => void | Promise<void>\n    ): void;\n\n    /** Shorthand property for ignoring a particular test case.\n     *\n     * @category Testing\n     */\n    ignore(t: Omit<TestDefinition, \"ignore\">): void;\n\n    /** Shorthand property for ignoring a particular test case.\n     *\n     * @category Testing\n     */\n    ignore(name: string, fn: (t: TestContext) => void | Promise<void>): void;\n\n    /** Shorthand property for ignoring a particular test case.\n     *\n     * @category Testing\n     */\n    ignore(fn: (t: TestContext) => void | Promise<void>): void;\n\n    /** Shorthand property for ignoring a particular test case.\n     *\n     * @category Testing\n     */\n    ignore(\n      name: string,\n      options: Omit<TestDefinition, \"fn\" | \"name\" | \"ignore\">,\n      fn: (t: TestContext) => void | Promise<void>\n    ): void;\n\n    /** Shorthand property for ignoring a particular test case.\n     *\n     * @category Testing\n     */\n    ignore(\n      options: Omit<TestDefinition, \"fn\" | \"name\" | \"ignore\">,\n      fn: (t: TestContext) => void | Promise<void>\n    ): void;\n\n    /** Shorthand property for ignoring a particular test case.\n     *\n     * @category Testing\n     */\n    ignore(\n      options: Omit<TestDefinition, \"fn\" | \"ignore\">,\n      fn: (t: TestContext) => void | Promise<void>\n    ): void;\n\n    /** Shorthand property for focusing a particular test case.\n     *\n     * @category Testing\n     */\n    only(t: Omit<TestDefinition, \"only\">): void;\n\n    /** Shorthand property for focusing a particular test case.\n     *\n     * @category Testing\n     */\n    only(name: string, fn: (t: TestContext) => void | Promise<void>): void;\n\n    /** Shorthand property for focusing a particular test case.\n     *\n     * @category Testing\n     */\n    only(fn: (t: TestContext) => void | Promise<void>): void;\n\n    /** Shorthand property for focusing a particular test case.\n     *\n     * @category Testing\n     */\n    only(\n      name: string,\n      options: Omit<TestDefinition, \"fn\" | \"name\" | \"only\">,\n      fn: (t: TestContext) => void | Promise<void>\n    ): void;\n\n    /** Shorthand property for focusing a particular test case.\n     *\n     * @category Testing\n     */\n    only(\n      options: Omit<TestDefinition, \"fn\" | \"name\" | \"only\">,\n      fn: (t: TestContext) => void | Promise<void>\n    ): void;\n\n    /** Shorthand property for focusing a particular test case.\n     *\n     * @category Testing\n     */\n    only(\n      options: Omit<TestDefinition, \"fn\" | \"only\">,\n      fn: (t: TestContext) => void | Promise<void>\n    ): void;\n  }\n\n  /**\n   * Context that is passed to a benchmarked function. The instance is shared\n   * between iterations of the benchmark. Its methods can be used for example\n   * to override of the measured portion of the function.\n   *\n   * @category Testing\n   */\n  export interface BenchContext {\n    /** The current benchmark name. */\n    name: string;\n    /** The string URL of the current benchmark. */\n    origin: string;\n\n    /** Restarts the timer for the bench measurement. This should be called\n     * after doing setup work which should not be measured.\n     *\n     * Warning: This method should not be used for benchmarks averaging less\n     * than 10μs per iteration. In such cases it will be disabled but the call\n     * will still have noticeable overhead, resulting in a warning.\n     *\n     * ```ts\n     * Deno.bench(\"foo\", async (t) => {\n     *   const data = await Deno.readFile(\"data.txt\");\n     *   t.start();\n     *   // some operation on `data`...\n     * });\n     * ```\n     */\n    start(): void;\n\n    /** End the timer early for the bench measurement. This should be called\n     * before doing teardown work which should not be measured.\n     *\n     * Warning: This method should not be used for benchmarks averaging less\n     * than 10μs per iteration. In such cases it will be disabled but the call\n     * will still have noticeable overhead, resulting in a warning.\n     *\n     * ```ts\n     * Deno.bench(\"foo\", async (t) => {\n     *   using file = await Deno.open(\"data.txt\");\n     *   t.start();\n     *   // some operation on `file`...\n     *   t.end();\n     * });\n     * ```\n     */\n    end(): void;\n  }\n\n  /**\n   * The interface for defining a benchmark test using {@linkcode Deno.bench}.\n   *\n   * @category Testing\n   */\n  export interface BenchDefinition {\n    /** The test function which will be benchmarked. */\n    fn: (b: BenchContext) => void | Promise<void>;\n    /** The name of the test, which will be used in displaying the results. */\n    name: string;\n    /** If truthy, the benchmark test will be ignored/skipped. */\n    ignore?: boolean;\n    /** Group name for the benchmark.\n     *\n     * Grouped benchmarks produce a group time summary, where the difference\n     * in performance between each test of the group is compared. */\n    group?: string;\n    /** Benchmark should be used as the baseline for other benchmarks.\n     *\n     * If there are multiple baselines in a group, the first one is used as the\n     * baseline. */\n    baseline?: boolean;\n    /** If at least one bench has `only` set to true, only run benches that have\n     * `only` set to `true` and fail the bench suite. */\n    only?: boolean;\n    /** Ensure the bench case does not prematurely cause the process to exit,\n     * for example via a call to {@linkcode Deno.exit}.\n     *\n     * @default {true} */\n    sanitizeExit?: boolean;\n    /** Specifies the permissions that should be used to run the bench.\n     *\n     * Set this to `\"inherit\"` to keep the calling thread's permissions.\n     *\n     * Set this to `\"none\"` to revoke all permissions.\n     *\n     * @default {\"inherit\"}\n     */\n    permissions?: PermissionOptions;\n  }\n\n  /**\n   * Register a benchmark test which will be run when `deno bench` is used on\n   * the command line and the containing module looks like a bench module.\n   *\n   * If the test function (`fn`) returns a promise or is async, the test runner\n   * will await resolution to consider the test complete.\n   *\n   * ```ts\n   * import { assertEquals } from \"jsr:@std/assert\";\n   *\n   * Deno.bench({\n   *   name: \"example test\",\n   *   fn() {\n   *     assertEquals(\"world\", \"world\");\n   *   },\n   * });\n   *\n   * Deno.bench({\n   *   name: \"example ignored test\",\n   *   ignore: Deno.build.os === \"windows\",\n   *   fn() {\n   *     // This test is ignored only on Windows machines\n   *   },\n   * });\n   *\n   * Deno.bench({\n   *   name: \"example async test\",\n   *   async fn() {\n   *     const decoder = new TextDecoder(\"utf-8\");\n   *     const data = await Deno.readFile(\"hello_world.txt\");\n   *     assertEquals(decoder.decode(data), \"Hello world\");\n   *   }\n   * });\n   * ```\n   *\n   * @category Testing\n   */\n  export function bench(b: BenchDefinition): void;\n\n  /**\n   * Register a benchmark test which will be run when `deno bench` is used on\n   * the command line and the containing module looks like a bench module.\n   *\n   * If the test function (`fn`) returns a promise or is async, the test runner\n   * will await resolution to consider the test complete.\n   *\n   * ```ts\n   * import { assertEquals } from \"jsr:@std/assert\";\n   *\n   * Deno.bench(\"My test description\", () => {\n   *   assertEquals(\"hello\", \"hello\");\n   * });\n   *\n   * Deno.bench(\"My async test description\", async () => {\n   *   const decoder = new TextDecoder(\"utf-8\");\n   *   const data = await Deno.readFile(\"hello_world.txt\");\n   *   assertEquals(decoder.decode(data), \"Hello world\");\n   * });\n   * ```\n   *\n   * @category Testing\n   */\n  export function bench(\n    name: string,\n    fn: (b: BenchContext) => void | Promise<void>\n  ): void;\n\n  /**\n   * Register a benchmark test which will be run when `deno bench` is used on\n   * the command line and the containing module looks like a bench module.\n   *\n   * If the test function (`fn`) returns a promise or is async, the test runner\n   * will await resolution to consider the test complete.\n   *\n   * ```ts\n   * import { assertEquals } from \"jsr:@std/assert\";\n   *\n   * Deno.bench(function myTestName() {\n   *   assertEquals(\"hello\", \"hello\");\n   * });\n   *\n   * Deno.bench(async function myOtherTestName() {\n   *   const decoder = new TextDecoder(\"utf-8\");\n   *   const data = await Deno.readFile(\"hello_world.txt\");\n   *   assertEquals(decoder.decode(data), \"Hello world\");\n   * });\n   * ```\n   *\n   * @category Testing\n   */\n  export function bench(fn: (b: BenchContext) => void | Promise<void>): void;\n\n  /**\n   * Register a benchmark test which will be run when `deno bench` is used on\n   * the command line and the containing module looks like a bench module.\n   *\n   * If the test function (`fn`) returns a promise or is async, the test runner\n   * will await resolution to consider the test complete.\n   *\n   * ```ts\n   * import { assertEquals } from \"jsr:@std/assert\";\n   *\n   * Deno.bench(\n   *   \"My test description\",\n   *   { permissions: { read: true } },\n   *   () => {\n   *    assertEquals(\"hello\", \"hello\");\n   *   }\n   * );\n   *\n   * Deno.bench(\n   *   \"My async test description\",\n   *   { permissions: { read: false } },\n   *   async () => {\n   *     const decoder = new TextDecoder(\"utf-8\");\n   *     const data = await Deno.readFile(\"hello_world.txt\");\n   *     assertEquals(decoder.decode(data), \"Hello world\");\n   *   }\n   * );\n   * ```\n   *\n   * @category Testing\n   */\n  export function bench(\n    name: string,\n    options: Omit<BenchDefinition, \"fn\" | \"name\">,\n    fn: (b: BenchContext) => void | Promise<void>\n  ): void;\n\n  /**\n   * Register a benchmark test which will be run when `deno bench` is used on\n   * the command line and the containing module looks like a bench module.\n   *\n   * If the test function (`fn`) returns a promise or is async, the test runner\n   * will await resolution to consider the test complete.\n   *\n   * ```ts\n   * import { assertEquals } from \"jsr:@std/assert\";\n   *\n   * Deno.bench(\n   *   { name: \"My test description\", permissions: { read: true } },\n   *   () => {\n   *     assertEquals(\"hello\", \"hello\");\n   *   }\n   * );\n   *\n   * Deno.bench(\n   *   { name: \"My async test description\", permissions: { read: false } },\n   *   async () => {\n   *     const decoder = new TextDecoder(\"utf-8\");\n   *     const data = await Deno.readFile(\"hello_world.txt\");\n   *     assertEquals(decoder.decode(data), \"Hello world\");\n   *   }\n   * );\n   * ```\n   *\n   * @category Testing\n   */\n  export function bench(\n    options: Omit<BenchDefinition, \"fn\">,\n    fn: (b: BenchContext) => void | Promise<void>\n  ): void;\n\n  /**\n   * Register a benchmark test which will be run when `deno bench` is used on\n   * the command line and the containing module looks like a bench module.\n   *\n   * If the test function (`fn`) returns a promise or is async, the test runner\n   * will await resolution to consider the test complete.\n   *\n   * ```ts\n   * import { assertEquals } from \"jsr:@std/assert\";\n   *\n   * Deno.bench(\n   *   { permissions: { read: true } },\n   *   function myTestName() {\n   *     assertEquals(\"hello\", \"hello\");\n   *   }\n   * );\n   *\n   * Deno.bench(\n   *   { permissions: { read: false } },\n   *   async function myOtherTestName() {\n   *     const decoder = new TextDecoder(\"utf-8\");\n   *     const data = await Deno.readFile(\"hello_world.txt\");\n   *     assertEquals(decoder.decode(data), \"Hello world\");\n   *   }\n   * );\n   * ```\n   *\n   * @category Testing\n   */\n  export function bench(\n    options: Omit<BenchDefinition, \"fn\" | \"name\">,\n    fn: (b: BenchContext) => void | Promise<void>\n  ): void;\n\n  /** Exit the Deno process with optional exit code.\n   *\n   * If no exit code is supplied then Deno will exit with return code of `0`.\n   *\n   * In worker contexts this is an alias to `self.close();`.\n   *\n   * ```ts\n   * Deno.exit(5);\n   * ```\n   *\n   * @category Runtime\n   */\n  export function exit(code?: number): never;\n\n  /** The exit code for the Deno process.\n   *\n   * If no exit code has been supplied, then Deno will assume a return code of `0`.\n   *\n   * When setting an exit code value, a number or non-NaN string must be provided,\n   * otherwise a TypeError will be thrown.\n   *\n   * ```ts\n   * console.log(Deno.exitCode); //-> 0\n   * Deno.exitCode = 1;\n   * console.log(Deno.exitCode); //-> 1\n   * ```\n   *\n   * @category Runtime\n   */\n  export var exitCode: number;\n\n  /** An interface containing methods to interact with the process environment\n   * variables.\n   *\n   * @tags allow-env\n   * @category Runtime\n   */\n  export interface Env {\n    /** Retrieve the value of an environment variable.\n     *\n     * Returns `undefined` if the supplied environment variable is not defined.\n     *\n     * ```ts\n     * console.log(Deno.env.get(\"HOME\"));  // e.g. outputs \"/home/alice\"\n     * console.log(Deno.env.get(\"MADE_UP_VAR\"));  // outputs \"undefined\"\n     * ```\n     *\n     * Requires `allow-env` permission.\n     *\n     * @tags allow-env\n     */\n    get(key: string): string | undefined;\n\n    /** Set the value of an environment variable.\n     *\n     * ```ts\n     * Deno.env.set(\"SOME_VAR\", \"Value\");\n     * Deno.env.get(\"SOME_VAR\");  // outputs \"Value\"\n     * ```\n     *\n     * Requires `allow-env` permission.\n     *\n     * @tags allow-env\n     */\n    set(key: string, value: string): void;\n\n    /** Delete the value of an environment variable.\n     *\n     * ```ts\n     * Deno.env.set(\"SOME_VAR\", \"Value\");\n     * Deno.env.delete(\"SOME_VAR\");  // outputs \"undefined\"\n     * ```\n     *\n     * Requires `allow-env` permission.\n     *\n     * @tags allow-env\n     */\n    delete(key: string): void;\n\n    /** Check whether an environment variable is present or not.\n     *\n     * ```ts\n     * Deno.env.set(\"SOME_VAR\", \"Value\");\n     * Deno.env.has(\"SOME_VAR\");  // outputs true\n     * ```\n     *\n     * Requires `allow-env` permission.\n     *\n     * @tags allow-env\n     */\n    has(key: string): boolean;\n\n    /** Returns a snapshot of the environment variables at invocation as a\n     * simple object of keys and values.\n     *\n     * ```ts\n     * Deno.env.set(\"TEST_VAR\", \"A\");\n     * const myEnv = Deno.env.toObject();\n     * console.log(myEnv.SHELL);\n     * Deno.env.set(\"TEST_VAR\", \"B\");\n     * console.log(myEnv.TEST_VAR);  // outputs \"A\"\n     * ```\n     *\n     * Requires `allow-env` permission.\n     *\n     * @tags allow-env\n     */\n    toObject(): { [index: string]: string };\n  }\n\n  /** An interface containing methods to interact with the process environment\n   * variables.\n   *\n   * @tags allow-env\n   * @category Runtime\n   */\n  export const env: Env;\n\n  /**\n   * Returns the path to the current deno executable.\n   *\n   * ```ts\n   * console.log(Deno.execPath());  // e.g. \"/home/alice/.local/bin/deno\"\n   * ```\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category Runtime\n   */\n  export function execPath(): string;\n\n  /**\n   * Change the current working directory to the specified path.\n   *\n   * ```ts\n   * Deno.chdir(\"/home/userA\");\n   * Deno.chdir(\"../userB\");\n   * Deno.chdir(\"C:\\\\Program Files (x86)\\\\Java\");\n   * ```\n   *\n   * Throws {@linkcode Deno.errors.NotFound} if directory not found.\n   *\n   * Throws {@linkcode Deno.errors.PermissionDenied} if the user does not have\n   * operating system file access rights.\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category Runtime\n   */\n  export function chdir(directory: string | URL): void;\n\n  /**\n   * Return a string representing the current working directory.\n   *\n   * If the current directory can be reached via multiple paths (due to symbolic\n   * links), `cwd()` may return any one of them.\n   *\n   * ```ts\n   * const currentWorkingDirectory = Deno.cwd();\n   * ```\n   *\n   * Throws {@linkcode Deno.errors.NotFound} if directory not available.\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category Runtime\n   */\n  export function cwd(): string;\n\n  /**\n   * Creates `newpath` as a hard link to `oldpath`.\n   *\n   * ```ts\n   * await Deno.link(\"old/name\", \"new/name\");\n   * ```\n   *\n   * Requires `allow-read` and `allow-write` permissions.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function link(oldpath: string, newpath: string): Promise<void>;\n\n  /**\n   * Synchronously creates `newpath` as a hard link to `oldpath`.\n   *\n   * ```ts\n   * Deno.linkSync(\"old/name\", \"new/name\");\n   * ```\n   *\n   * Requires `allow-read` and `allow-write` permissions.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function linkSync(oldpath: string, newpath: string): void;\n\n  /**\n   * A enum which defines the seek mode for IO related APIs that support\n   * seeking.\n   *\n   * @category I/O */\n  export enum SeekMode {\n    /* Seek from the start of the file/resource. */\n    Start = 0,\n    /* Seek from the current position within the file/resource. */\n    Current = 1,\n    /* Seek from the end of the current file/resource. */\n    End = 2,\n  }\n\n  /** Open a file and resolve to an instance of {@linkcode Deno.FsFile}. The\n   * file does not need to previously exist if using the `create` or `createNew`\n   * open options. The caller may have the resulting file automatically closed\n   * by the runtime once it's out of scope by declaring the file variable with\n   * the `using` keyword.\n   *\n   * ```ts\n   * using file = await Deno.open(\"/foo/bar.txt\", { read: true, write: true });\n   * // Do work with file\n   * ```\n   *\n   * Alternatively, the caller may manually close the resource when finished with\n   * it.\n   *\n   * ```ts\n   * const file = await Deno.open(\"/foo/bar.txt\", { read: true, write: true });\n   * // Do work with file\n   * file.close();\n   * ```\n   *\n   * Requires `allow-read` and/or `allow-write` permissions depending on\n   * options.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function open(\n    path: string | URL,\n    options?: OpenOptions\n  ): Promise<FsFile>;\n\n  /** Synchronously open a file and return an instance of\n   * {@linkcode Deno.FsFile}. The file does not need to previously exist if\n   * using the `create` or `createNew` open options. The caller may have the\n   * resulting file automatically closed by the runtime once it's out of scope\n   * by declaring the file variable with the `using` keyword.\n   *\n   * ```ts\n   * using file = Deno.openSync(\"/foo/bar.txt\", { read: true, write: true });\n   * // Do work with file\n   * ```\n   *\n   * Alternatively, the caller may manually close the resource when finished with\n   * it.\n   *\n   * ```ts\n   * const file = Deno.openSync(\"/foo/bar.txt\", { read: true, write: true });\n   * // Do work with file\n   * file.close();\n   * ```\n   *\n   * Requires `allow-read` and/or `allow-write` permissions depending on\n   * options.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function openSync(path: string | URL, options?: OpenOptions): FsFile;\n\n  /** Creates a file if none exists or truncates an existing file and resolves to\n   *  an instance of {@linkcode Deno.FsFile}.\n   *\n   * ```ts\n   * const file = await Deno.create(\"/foo/bar.txt\");\n   * ```\n   *\n   * Requires `allow-read` and `allow-write` permissions.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function create(path: string | URL): Promise<FsFile>;\n\n  /** Creates a file if none exists or truncates an existing file and returns\n   *  an instance of {@linkcode Deno.FsFile}.\n   *\n   * ```ts\n   * const file = Deno.createSync(\"/foo/bar.txt\");\n   * ```\n   *\n   * Requires `allow-read` and `allow-write` permissions.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function createSync(path: string | URL): FsFile;\n\n  /** The Deno abstraction for reading and writing files.\n   *\n   * This is the most straight forward way of handling files within Deno and is\n   * recommended over using the discrete functions within the `Deno` namespace.\n   *\n   * ```ts\n   * using file = await Deno.open(\"/foo/bar.txt\", { read: true });\n   * const fileInfo = await file.stat();\n   * if (fileInfo.isFile) {\n   *   const buf = new Uint8Array(100);\n   *   const numberOfBytesRead = await file.read(buf); // 11 bytes\n   *   const text = new TextDecoder().decode(buf);  // \"hello world\"\n   * }\n   * ```\n   *\n   * @category File System\n   */\n  export class FsFile implements Disposable {\n    /** A {@linkcode ReadableStream} instance representing to the byte contents\n     * of the file. This makes it easy to interoperate with other web streams\n     * based APIs.\n     *\n     * ```ts\n     * using file = await Deno.open(\"my_file.txt\", { read: true });\n     * const decoder = new TextDecoder();\n     * for await (const chunk of file.readable) {\n     *   console.log(decoder.decode(chunk));\n     * }\n     * ```\n     */\n    readonly readable: ReadableStream<Uint8Array>;\n    /** A {@linkcode WritableStream} instance to write the contents of the\n     * file. This makes it easy to interoperate with other web streams based\n     * APIs.\n     *\n     * ```ts\n     * const items = [\"hello\", \"world\"];\n     * using file = await Deno.open(\"my_file.txt\", { write: true });\n     * const encoder = new TextEncoder();\n     * const writer = file.writable.getWriter();\n     * for (const item of items) {\n     *   await writer.write(encoder.encode(item));\n     * }\n     * ```\n     */\n    readonly writable: WritableStream<Uint8Array>;\n    /** Write the contents of the array buffer (`p`) to the file.\n     *\n     * Resolves to the number of bytes written.\n     *\n     * **It is not guaranteed that the full buffer will be written in a single\n     * call.**\n     *\n     * ```ts\n     * const encoder = new TextEncoder();\n     * const data = encoder.encode(\"Hello world\");\n     * using file = await Deno.open(\"/foo/bar.txt\", { write: true });\n     * const bytesWritten = await file.write(data); // 11\n     * ```\n     *\n     * @category I/O\n     */\n    write(p: Uint8Array): Promise<number>;\n    /** Synchronously write the contents of the array buffer (`p`) to the file.\n     *\n     * Returns the number of bytes written.\n     *\n     * **It is not guaranteed that the full buffer will be written in a single\n     * call.**\n     *\n     * ```ts\n     * const encoder = new TextEncoder();\n     * const data = encoder.encode(\"Hello world\");\n     * using file = Deno.openSync(\"/foo/bar.txt\", { write: true });\n     * const bytesWritten = file.writeSync(data); // 11\n     * ```\n     */\n    writeSync(p: Uint8Array): number;\n    /** Truncates (or extends) the file to reach the specified `len`. If `len`\n     * is not specified, then the entire file contents are truncated.\n     *\n     * ### Truncate the entire file\n     *\n     * ```ts\n     * using file = await Deno.open(\"my_file.txt\", { write: true });\n     * await file.truncate();\n     * ```\n     *\n     * ### Truncate part of the file\n     *\n     * ```ts\n     * // if \"my_file.txt\" contains the text \"hello world\":\n     * using file = await Deno.open(\"my_file.txt\", { write: true });\n     * await file.truncate(7);\n     * const buf = new Uint8Array(100);\n     * await file.read(buf);\n     * const text = new TextDecoder().decode(buf); // \"hello w\"\n     * ```\n     */\n    truncate(len?: number): Promise<void>;\n    /** Synchronously truncates (or extends) the file to reach the specified\n     * `len`. If `len` is not specified, then the entire file contents are\n     * truncated.\n     *\n     * ### Truncate the entire file\n     *\n     * ```ts\n     * using file = Deno.openSync(\"my_file.txt\", { write: true });\n     * file.truncateSync();\n     * ```\n     *\n     * ### Truncate part of the file\n     *\n     * ```ts\n     * // if \"my_file.txt\" contains the text \"hello world\":\n     * using file = Deno.openSync(\"my_file.txt\", { write: true });\n     * file.truncateSync(7);\n     * const buf = new Uint8Array(100);\n     * file.readSync(buf);\n     * const text = new TextDecoder().decode(buf); // \"hello w\"\n     * ```\n     */\n    truncateSync(len?: number): void;\n    /** Read the file into an array buffer (`p`).\n     *\n     * Resolves to either the number of bytes read during the operation or EOF\n     * (`null`) if there was nothing more to read.\n     *\n     * It is possible for a read to successfully return with `0` bytes. This\n     * does not indicate EOF.\n     *\n     * **It is not guaranteed that the full buffer will be read in a single\n     * call.**\n     *\n     * ```ts\n     * // if \"/foo/bar.txt\" contains the text \"hello world\":\n     * using file = await Deno.open(\"/foo/bar.txt\");\n     * const buf = new Uint8Array(100);\n     * const numberOfBytesRead = await file.read(buf); // 11 bytes\n     * const text = new TextDecoder().decode(buf);  // \"hello world\"\n     * ```\n     */\n    read(p: Uint8Array): Promise<number | null>;\n    /** Synchronously read from the file into an array buffer (`p`).\n     *\n     * Returns either the number of bytes read during the operation or EOF\n     * (`null`) if there was nothing more to read.\n     *\n     * It is possible for a read to successfully return with `0` bytes. This\n     * does not indicate EOF.\n     *\n     * **It is not guaranteed that the full buffer will be read in a single\n     * call.**\n     *\n     * ```ts\n     * // if \"/foo/bar.txt\" contains the text \"hello world\":\n     * using file = Deno.openSync(\"/foo/bar.txt\");\n     * const buf = new Uint8Array(100);\n     * const numberOfBytesRead = file.readSync(buf); // 11 bytes\n     * const text = new TextDecoder().decode(buf);  // \"hello world\"\n     * ```\n     */\n    readSync(p: Uint8Array): number | null;\n    /** Seek to the given `offset` under mode given by `whence`. The call\n     * resolves to the new position within the resource (bytes from the start).\n     *\n     * ```ts\n     * // Given the file contains \"Hello world\" text, which is 11 bytes long:\n     * using file = await Deno.open(\n     *   \"hello.txt\",\n     *   { read: true, write: true, truncate: true, create: true },\n     * );\n     * await file.write(new TextEncoder().encode(\"Hello world\"));\n     *\n     * // advance cursor 6 bytes\n     * const cursorPosition = await file.seek(6, Deno.SeekMode.Start);\n     * console.log(cursorPosition);  // 6\n     * const buf = new Uint8Array(100);\n     * await file.read(buf);\n     * console.log(new TextDecoder().decode(buf)); // \"world\"\n     * ```\n     *\n     * The seek modes work as follows:\n     *\n     * ```ts\n     * // Given the file contains \"Hello world\" text, which is 11 bytes long:\n     * const file = await Deno.open(\n     *   \"hello.txt\",\n     *   { read: true, write: true, truncate: true, create: true },\n     * );\n     * await file.write(new TextEncoder().encode(\"Hello world\"));\n     *\n     * // Seek 6 bytes from the start of the file\n     * console.log(await file.seek(6, Deno.SeekMode.Start)); // \"6\"\n     * // Seek 2 more bytes from the current position\n     * console.log(await file.seek(2, Deno.SeekMode.Current)); // \"8\"\n     * // Seek backwards 2 bytes from the end of the file\n     * console.log(await file.seek(-2, Deno.SeekMode.End)); // \"9\" (i.e. 11-2)\n     * ```\n     */\n    seek(offset: number | bigint, whence: SeekMode): Promise<number>;\n    /** Synchronously seek to the given `offset` under mode given by `whence`.\n     * The new position within the resource (bytes from the start) is returned.\n     *\n     * ```ts\n     * using file = Deno.openSync(\n     *   \"hello.txt\",\n     *   { read: true, write: true, truncate: true, create: true },\n     * );\n     * file.writeSync(new TextEncoder().encode(\"Hello world\"));\n     *\n     * // advance cursor 6 bytes\n     * const cursorPosition = file.seekSync(6, Deno.SeekMode.Start);\n     * console.log(cursorPosition);  // 6\n     * const buf = new Uint8Array(100);\n     * file.readSync(buf);\n     * console.log(new TextDecoder().decode(buf)); // \"world\"\n     * ```\n     *\n     * The seek modes work as follows:\n     *\n     * ```ts\n     * // Given the file contains \"Hello world\" text, which is 11 bytes long:\n     * using file = Deno.openSync(\n     *   \"hello.txt\",\n     *   { read: true, write: true, truncate: true, create: true },\n     * );\n     * file.writeSync(new TextEncoder().encode(\"Hello world\"));\n     *\n     * // Seek 6 bytes from the start of the file\n     * console.log(file.seekSync(6, Deno.SeekMode.Start)); // \"6\"\n     * // Seek 2 more bytes from the current position\n     * console.log(file.seekSync(2, Deno.SeekMode.Current)); // \"8\"\n     * // Seek backwards 2 bytes from the end of the file\n     * console.log(file.seekSync(-2, Deno.SeekMode.End)); // \"9\" (i.e. 11-2)\n     * ```\n     */\n    seekSync(offset: number | bigint, whence: SeekMode): number;\n    /** Resolves to a {@linkcode Deno.FileInfo} for the file.\n     *\n     * ```ts\n     * import { assert } from \"jsr:@std/assert\";\n     *\n     * using file = await Deno.open(\"hello.txt\");\n     * const fileInfo = await file.stat();\n     * assert(fileInfo.isFile);\n     * ```\n     */\n    stat(): Promise<FileInfo>;\n    /** Synchronously returns a {@linkcode Deno.FileInfo} for the file.\n     *\n     * ```ts\n     * import { assert } from \"jsr:@std/assert\";\n     *\n     * using file = Deno.openSync(\"hello.txt\")\n     * const fileInfo = file.statSync();\n     * assert(fileInfo.isFile);\n     * ```\n     */\n    statSync(): FileInfo;\n    /**\n     * Flushes any pending data and metadata operations of the given file\n     * stream to disk.\n     *\n     * ```ts\n     * const file = await Deno.open(\n     *   \"my_file.txt\",\n     *   { read: true, write: true, create: true },\n     * );\n     * await file.write(new TextEncoder().encode(\"Hello World\"));\n     * await file.truncate(1);\n     * await file.sync();\n     * console.log(await Deno.readTextFile(\"my_file.txt\")); // H\n     * ```\n     *\n     * @category I/O\n     */\n    sync(): Promise<void>;\n    /**\n     * Synchronously flushes any pending data and metadata operations of the given\n     * file stream to disk.\n     *\n     * ```ts\n     * const file = Deno.openSync(\n     *   \"my_file.txt\",\n     *   { read: true, write: true, create: true },\n     * );\n     * file.writeSync(new TextEncoder().encode(\"Hello World\"));\n     * file.truncateSync(1);\n     * file.syncSync();\n     * console.log(Deno.readTextFileSync(\"my_file.txt\")); // H\n     * ```\n     *\n     * @category I/O\n     */\n    syncSync(): void;\n    /**\n     * Flushes any pending data operations of the given file stream to disk.\n     *  ```ts\n     * using file = await Deno.open(\n     *   \"my_file.txt\",\n     *   { read: true, write: true, create: true },\n     * );\n     * await file.write(new TextEncoder().encode(\"Hello World\"));\n     * await file.syncData();\n     * console.log(await Deno.readTextFile(\"my_file.txt\")); // Hello World\n     * ```\n     *\n     * @category I/O\n     */\n    syncData(): Promise<void>;\n    /**\n     * Synchronously flushes any pending data operations of the given file stream\n     * to disk.\n     *\n     *  ```ts\n     * using file = Deno.openSync(\n     *   \"my_file.txt\",\n     *   { read: true, write: true, create: true },\n     * );\n     * file.writeSync(new TextEncoder().encode(\"Hello World\"));\n     * file.syncDataSync();\n     * console.log(Deno.readTextFileSync(\"my_file.txt\")); // Hello World\n     * ```\n     *\n     * @category I/O\n     */\n    syncDataSync(): void;\n    /**\n     * Changes the access (`atime`) and modification (`mtime`) times of the\n     * file stream resource. Given times are either in seconds (UNIX epoch\n     * time) or as `Date` objects.\n     *\n     * ```ts\n     * using file = await Deno.open(\"file.txt\", { create: true, write: true });\n     * await file.utime(1556495550, new Date());\n     * ```\n     *\n     * @category File System\n     */\n    utime(atime: number | Date, mtime: number | Date): Promise<void>;\n    /**\n     * Synchronously changes the access (`atime`) and modification (`mtime`)\n     * times of the file stream resource. Given times are either in seconds\n     * (UNIX epoch time) or as `Date` objects.\n     *\n     * ```ts\n     * using file = Deno.openSync(\"file.txt\", { create: true, write: true });\n     * file.utime(1556495550, new Date());\n     * ```\n     *\n     * @category File System\n     */\n    utimeSync(atime: number | Date, mtime: number | Date): void;\n    /** **UNSTABLE**: New API, yet to be vetted.\n     *\n     * Checks if the file resource is a TTY (terminal).\n     *\n     * ```ts\n     * // This example is system and context specific\n     * using file = await Deno.open(\"/dev/tty6\");\n     * file.isTerminal(); // true\n     * ```\n     */\n    isTerminal(): boolean;\n    /** **UNSTABLE**: New API, yet to be vetted.\n     *\n     * Set TTY to be under raw mode or not. In raw mode, characters are read and\n     * returned as is, without being processed. All special processing of\n     * characters by the terminal is disabled, including echoing input\n     * characters. Reading from a TTY device in raw mode is faster than reading\n     * from a TTY device in canonical mode.\n     *\n     * ```ts\n     * using file = await Deno.open(\"/dev/tty6\");\n     * file.setRaw(true, { cbreak: true });\n     * ```\n     */\n    setRaw(mode: boolean, options?: SetRawOptions): void;\n    /**\n     * Acquire an advisory file-system lock for the file.\n     *\n     * @param [exclusive=false]\n     */\n    lock(exclusive?: boolean): Promise<void>;\n    /**\n     * Synchronously acquire an advisory file-system lock synchronously for the file.\n     *\n     * @param [exclusive=false]\n     */\n    lockSync(exclusive?: boolean): void;\n    /**\n     * Release an advisory file-system lock for the file.\n     */\n    unlock(): Promise<void>;\n    /**\n     * Synchronously release an advisory file-system lock for the file.\n     */\n    unlockSync(): void;\n    /** Close the file. Closing a file when you are finished with it is\n     * important to avoid leaking resources.\n     *\n     * ```ts\n     * using file = await Deno.open(\"my_file.txt\");\n     * // do work with \"file\" object\n     * ```\n     */\n    close(): void;\n\n    [Symbol.dispose](): void;\n  }\n\n  /** Gets the size of the console as columns/rows.\n   *\n   * ```ts\n   * const { columns, rows } = Deno.consoleSize();\n   * ```\n   *\n   * This returns the size of the console window as reported by the operating\n   * system. It's not a reflection of how many characters will fit within the\n   * console window, but can be used as part of that calculation.\n   *\n   * @category I/O\n   */\n  export function consoleSize(): {\n    columns: number;\n    rows: number;\n  };\n\n  /** @category I/O */\n  export interface SetRawOptions {\n    /**\n     * The `cbreak` option can be used to indicate that characters that\n     * correspond to a signal should still be generated. When disabling raw\n     * mode, this option is ignored. This functionality currently only works on\n     * Linux and Mac OS.\n     */\n    cbreak: boolean;\n  }\n\n  /** A reference to `stdin` which can be used to read directly from `stdin`.\n   *\n   * It implements the Deno specific\n   * {@linkcode https://jsr.io/@std/io/doc/types/~/Reader | Reader},\n   * {@linkcode https://jsr.io/@std/io/doc/types/~/ReaderSync | ReaderSync},\n   * and {@linkcode https://jsr.io/@std/io/doc/types/~/Closer | Closer}\n   * interfaces as well as provides a {@linkcode ReadableStream} interface.\n   *\n   * ### Reading chunks from the readable stream\n   *\n   * ```ts\n   * const decoder = new TextDecoder();\n   * for await (const chunk of Deno.stdin.readable) {\n   *   const text = decoder.decode(chunk);\n   *   // do something with the text\n   * }\n   * ```\n   *\n   * @category I/O\n   */\n  export const stdin: {\n    /** Read the incoming data from `stdin` into an array buffer (`p`).\n     *\n     * Resolves to either the number of bytes read during the operation or EOF\n     * (`null`) if there was nothing more to read.\n     *\n     * It is possible for a read to successfully return with `0` bytes. This\n     * does not indicate EOF.\n     *\n     * **It is not guaranteed that the full buffer will be read in a single\n     * call.**\n     *\n     * ```ts\n     * // If the text \"hello world\" is piped into the script:\n     * const buf = new Uint8Array(100);\n     * const numberOfBytesRead = await Deno.stdin.read(buf); // 11 bytes\n     * const text = new TextDecoder().decode(buf);  // \"hello world\"\n     * ```\n     *\n     * @category I/O\n     */\n    read(p: Uint8Array): Promise<number | null>;\n    /** Synchronously read from the incoming data from `stdin` into an array\n     * buffer (`p`).\n     *\n     * Returns either the number of bytes read during the operation or EOF\n     * (`null`) if there was nothing more to read.\n     *\n     * It is possible for a read to successfully return with `0` bytes. This\n     * does not indicate EOF.\n     *\n     * **It is not guaranteed that the full buffer will be read in a single\n     * call.**\n     *\n     * ```ts\n     * // If the text \"hello world\" is piped into the script:\n     * const buf = new Uint8Array(100);\n     * const numberOfBytesRead = Deno.stdin.readSync(buf); // 11 bytes\n     * const text = new TextDecoder().decode(buf);  // \"hello world\"\n     * ```\n     *\n     * @category I/O\n     */\n    readSync(p: Uint8Array): number | null;\n    /** Closes `stdin`, freeing the resource.\n     *\n     * ```ts\n     * Deno.stdin.close();\n     * ```\n     */\n    close(): void;\n    /** A readable stream interface to `stdin`. */\n    readonly readable: ReadableStream<Uint8Array>;\n    /**\n     * Set TTY to be under raw mode or not. In raw mode, characters are read and\n     * returned as is, without being processed. All special processing of\n     * characters by the terminal is disabled, including echoing input\n     * characters. Reading from a TTY device in raw mode is faster than reading\n     * from a TTY device in canonical mode.\n     *\n     * ```ts\n     * Deno.stdin.setRaw(true, { cbreak: true });\n     * ```\n     *\n     * @category I/O\n     */\n    setRaw(mode: boolean, options?: SetRawOptions): void;\n    /**\n     * Checks if `stdin` is a TTY (terminal).\n     *\n     * ```ts\n     * // This example is system and context specific\n     * Deno.stdin.isTerminal(); // true\n     * ```\n     *\n     * @category I/O\n     */\n    isTerminal(): boolean;\n  };\n  /** A reference to `stdout` which can be used to write directly to `stdout`.\n   * It implements the Deno specific\n   * {@linkcode https://jsr.io/@std/io/doc/types/~/Writer | Writer},\n   * {@linkcode https://jsr.io/@std/io/doc/types/~/WriterSync | WriterSync},\n   * and {@linkcode https://jsr.io/@std/io/doc/types/~/Closer | Closer} interfaces as well as provides a\n   * {@linkcode WritableStream} interface.\n   *\n   * These are low level constructs, and the {@linkcode console} interface is a\n   * more straight forward way to interact with `stdout` and `stderr`.\n   *\n   * @category I/O\n   */\n  export const stdout: {\n    /** Write the contents of the array buffer (`p`) to `stdout`.\n     *\n     * Resolves to the number of bytes written.\n     *\n     * **It is not guaranteed that the full buffer will be written in a single\n     * call.**\n     *\n     * ```ts\n     * const encoder = new TextEncoder();\n     * const data = encoder.encode(\"Hello world\");\n     * const bytesWritten = await Deno.stdout.write(data); // 11\n     * ```\n     *\n     * @category I/O\n     */\n    write(p: Uint8Array): Promise<number>;\n    /** Synchronously write the contents of the array buffer (`p`) to `stdout`.\n     *\n     * Returns the number of bytes written.\n     *\n     * **It is not guaranteed that the full buffer will be written in a single\n     * call.**\n     *\n     * ```ts\n     * const encoder = new TextEncoder();\n     * const data = encoder.encode(\"Hello world\");\n     * const bytesWritten = Deno.stdout.writeSync(data); // 11\n     * ```\n     */\n    writeSync(p: Uint8Array): number;\n    /** Closes `stdout`, freeing the resource.\n     *\n     * ```ts\n     * Deno.stdout.close();\n     * ```\n     */\n    close(): void;\n    /** A writable stream interface to `stdout`. */\n    readonly writable: WritableStream<Uint8Array>;\n    /**\n     * Checks if `stdout` is a TTY (terminal).\n     *\n     * ```ts\n     * // This example is system and context specific\n     * Deno.stdout.isTerminal(); // true\n     * ```\n     *\n     * @category I/O\n     */\n    isTerminal(): boolean;\n  };\n  /** A reference to `stderr` which can be used to write directly to `stderr`.\n   * It implements the Deno specific\n   * {@linkcode https://jsr.io/@std/io/doc/types/~/Writer | Writer},\n   * {@linkcode https://jsr.io/@std/io/doc/types/~/WriterSync | WriterSync},\n   * and {@linkcode https://jsr.io/@std/io/doc/types/~/Closer | Closer} interfaces as well as provides a\n   * {@linkcode WritableStream} interface.\n   *\n   * These are low level constructs, and the {@linkcode console} interface is a\n   * more straight forward way to interact with `stdout` and `stderr`.\n   *\n   * @category I/O\n   */\n  export const stderr: {\n    /** Write the contents of the array buffer (`p`) to `stderr`.\n     *\n     * Resolves to the number of bytes written.\n     *\n     * **It is not guaranteed that the full buffer will be written in a single\n     * call.**\n     *\n     * ```ts\n     * const encoder = new TextEncoder();\n     * const data = encoder.encode(\"Hello world\");\n     * const bytesWritten = await Deno.stderr.write(data); // 11\n     * ```\n     *\n     * @category I/O\n     */\n    write(p: Uint8Array): Promise<number>;\n    /** Synchronously write the contents of the array buffer (`p`) to `stderr`.\n     *\n     * Returns the number of bytes written.\n     *\n     * **It is not guaranteed that the full buffer will be written in a single\n     * call.**\n     *\n     * ```ts\n     * const encoder = new TextEncoder();\n     * const data = encoder.encode(\"Hello world\");\n     * const bytesWritten = Deno.stderr.writeSync(data); // 11\n     * ```\n     */\n    writeSync(p: Uint8Array): number;\n    /** Closes `stderr`, freeing the resource.\n     *\n     * ```ts\n     * Deno.stderr.close();\n     * ```\n     */\n    close(): void;\n    /** A writable stream interface to `stderr`. */\n    readonly writable: WritableStream<Uint8Array>;\n    /**\n     * Checks if `stderr` is a TTY (terminal).\n     *\n     * ```ts\n     * // This example is system and context specific\n     * Deno.stderr.isTerminal(); // true\n     * ```\n     *\n     * @category I/O\n     */\n    isTerminal(): boolean;\n  };\n\n  /**\n   * Options which can be set when doing {@linkcode Deno.open} and\n   * {@linkcode Deno.openSync}.\n   *\n   * @category File System */\n  export interface OpenOptions {\n    /** Sets the option for read access. This option, when `true`, means that\n     * the file should be read-able if opened.\n     *\n     * @default {true} */\n    read?: boolean;\n    /** Sets the option for write access. This option, when `true`, means that\n     * the file should be write-able if opened. If the file already exists,\n     * any write calls on it will overwrite its contents, by default without\n     * truncating it.\n     *\n     * @default {false} */\n    write?: boolean;\n    /** Sets the option for the append mode. This option, when `true`, means\n     * that writes will append to a file instead of overwriting previous\n     * contents.\n     *\n     * Note that setting `{ write: true, append: true }` has the same effect as\n     * setting only `{ append: true }`.\n     *\n     * @default {false} */\n    append?: boolean;\n    /** Sets the option for truncating a previous file. If a file is\n     * successfully opened with this option set it will truncate the file to `0`\n     * size if it already exists. The file must be opened with write access\n     * for truncate to work.\n     *\n     * @default {false} */\n    truncate?: boolean;\n    /** Sets the option to allow creating a new file, if one doesn't already\n     * exist at the specified path. Requires write or append access to be\n     * used.\n     *\n     * @default {false} */\n    create?: boolean;\n    /** If set to `true`, no file, directory, or symlink is allowed to exist at\n     * the target location. Requires write or append access to be used. When\n     * createNew is set to `true`, create and truncate are ignored.\n     *\n     * @default {false} */\n    createNew?: boolean;\n    /** Permissions to use if creating the file (defaults to `0o666`, before\n     * the process's umask).\n     *\n     * Ignored on Windows. */\n    mode?: number;\n  }\n\n  /**\n   * Options which can be set when using {@linkcode Deno.readFile} or\n   * {@linkcode Deno.readFileSync}.\n   *\n   * @category File System */\n  export interface ReadFileOptions {\n    /**\n     * An abort signal to allow cancellation of the file read operation.\n     * If the signal becomes aborted the readFile operation will be stopped\n     * and the promise returned will be rejected with an AbortError.\n     */\n    signal?: AbortSignal;\n  }\n\n  /**\n   * Options which can be set when using {@linkcode Deno.mkdir} and\n   * {@linkcode Deno.mkdirSync}.\n   *\n   * @category File System */\n  export interface MkdirOptions {\n    /** If set to `true`, means that any intermediate directories will also be\n     * created (as with the shell command `mkdir -p`).\n     *\n     * Intermediate directories are created with the same permissions.\n     *\n     * When recursive is set to `true`, succeeds silently (without changing any\n     * permissions) if a directory already exists at the path, or if the path\n     * is a symlink to an existing directory.\n     *\n     * @default {false} */\n    recursive?: boolean;\n    /** Permissions to use when creating the directory (defaults to `0o777`,\n     * before the process's umask).\n     *\n     * Ignored on Windows. */\n    mode?: number;\n  }\n\n  /** Creates a new directory with the specified path.\n   *\n   * ```ts\n   * await Deno.mkdir(\"new_dir\");\n   * await Deno.mkdir(\"nested/directories\", { recursive: true });\n   * await Deno.mkdir(\"restricted_access_dir\", { mode: 0o700 });\n   * ```\n   *\n   * Defaults to throwing error if the directory already exists.\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function mkdir(\n    path: string | URL,\n    options?: MkdirOptions\n  ): Promise<void>;\n\n  /** Synchronously creates a new directory with the specified path.\n   *\n   * ```ts\n   * Deno.mkdirSync(\"new_dir\");\n   * Deno.mkdirSync(\"nested/directories\", { recursive: true });\n   * Deno.mkdirSync(\"restricted_access_dir\", { mode: 0o700 });\n   * ```\n   *\n   * Defaults to throwing error if the directory already exists.\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function mkdirSync(path: string | URL, options?: MkdirOptions): void;\n\n  /**\n   * Options which can be set when using {@linkcode Deno.makeTempDir},\n   * {@linkcode Deno.makeTempDirSync}, {@linkcode Deno.makeTempFile}, and\n   * {@linkcode Deno.makeTempFileSync}.\n   *\n   * @category File System */\n  export interface MakeTempOptions {\n    /** Directory where the temporary directory should be created (defaults to\n     * the env variable `TMPDIR`, or the system's default, usually `/tmp`).\n     *\n     * Note that if the passed `dir` is relative, the path returned by\n     * `makeTempFile()` and `makeTempDir()` will also be relative. Be mindful of\n     * this when changing working directory. */\n    dir?: string;\n    /** String that should precede the random portion of the temporary\n     * directory's name. */\n    prefix?: string;\n    /** String that should follow the random portion of the temporary\n     * directory's name. */\n    suffix?: string;\n  }\n\n  /** Creates a new temporary directory in the default directory for temporary\n   * files, unless `dir` is specified. Other optional options include\n   * prefixing and suffixing the directory name with `prefix` and `suffix`\n   * respectively.\n   *\n   * This call resolves to the full path to the newly created directory.\n   *\n   * Multiple programs calling this function simultaneously will create different\n   * directories. It is the caller's responsibility to remove the directory when\n   * no longer needed.\n   *\n   * ```ts\n   * const tempDirName0 = await Deno.makeTempDir();  // e.g. /tmp/2894ea76\n   * const tempDirName1 = await Deno.makeTempDir({ prefix: 'my_temp' }); // e.g. /tmp/my_temp339c944d\n   * ```\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  // TODO(ry) Doesn't check permissions.\n  export function makeTempDir(options?: MakeTempOptions): Promise<string>;\n\n  /** Synchronously creates a new temporary directory in the default directory\n   * for temporary files, unless `dir` is specified. Other optional options\n   * include prefixing and suffixing the directory name with `prefix` and\n   * `suffix` respectively.\n   *\n   * The full path to the newly created directory is returned.\n   *\n   * Multiple programs calling this function simultaneously will create different\n   * directories. It is the caller's responsibility to remove the directory when\n   * no longer needed.\n   *\n   * ```ts\n   * const tempDirName0 = Deno.makeTempDirSync();  // e.g. /tmp/2894ea76\n   * const tempDirName1 = Deno.makeTempDirSync({ prefix: 'my_temp' });  // e.g. /tmp/my_temp339c944d\n   * ```\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  // TODO(ry) Doesn't check permissions.\n  export function makeTempDirSync(options?: MakeTempOptions): string;\n\n  /** Creates a new temporary file in the default directory for temporary\n   * files, unless `dir` is specified.\n   *\n   * Other options include prefixing and suffixing the directory name with\n   * `prefix` and `suffix` respectively.\n   *\n   * This call resolves to the full path to the newly created file.\n   *\n   * Multiple programs calling this function simultaneously will create\n   * different files. It is the caller's responsibility to remove the file when\n   * no longer needed.\n   *\n   * ```ts\n   * const tmpFileName0 = await Deno.makeTempFile();  // e.g. /tmp/419e0bf2\n   * const tmpFileName1 = await Deno.makeTempFile({ prefix: 'my_temp' });  // e.g. /tmp/my_temp754d3098\n   * ```\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function makeTempFile(options?: MakeTempOptions): Promise<string>;\n\n  /** Synchronously creates a new temporary file in the default directory for\n   * temporary files, unless `dir` is specified.\n   *\n   * Other options include prefixing and suffixing the directory name with\n   * `prefix` and `suffix` respectively.\n   *\n   * The full path to the newly created file is returned.\n   *\n   * Multiple programs calling this function simultaneously will create\n   * different files. It is the caller's responsibility to remove the file when\n   * no longer needed.\n   *\n   * ```ts\n   * const tempFileName0 = Deno.makeTempFileSync(); // e.g. /tmp/419e0bf2\n   * const tempFileName1 = Deno.makeTempFileSync({ prefix: 'my_temp' });  // e.g. /tmp/my_temp754d3098\n   * ```\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function makeTempFileSync(options?: MakeTempOptions): string;\n\n  /** Changes the permission of a specific file/directory of specified path.\n   * Ignores the process's umask.\n   *\n   * ```ts\n   * await Deno.chmod(\"/path/to/file\", 0o666);\n   * ```\n   *\n   * The mode is a sequence of 3 octal numbers. The first/left-most number\n   * specifies the permissions for the owner. The second number specifies the\n   * permissions for the group. The last/right-most number specifies the\n   * permissions for others. For example, with a mode of 0o764, the owner (7)\n   * can read/write/execute, the group (6) can read/write and everyone else (4)\n   * can read only.\n   *\n   * | Number | Description |\n   * | ------ | ----------- |\n   * | 7      | read, write, and execute |\n   * | 6      | read and write |\n   * | 5      | read and execute |\n   * | 4      | read only |\n   * | 3      | write and execute |\n   * | 2      | write only |\n   * | 1      | execute only |\n   * | 0      | no permission |\n   *\n   * NOTE: This API currently throws on Windows\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function chmod(path: string | URL, mode: number): Promise<void>;\n\n  /** Synchronously changes the permission of a specific file/directory of\n   * specified path. Ignores the process's umask.\n   *\n   * ```ts\n   * Deno.chmodSync(\"/path/to/file\", 0o666);\n   * ```\n   *\n   * For a full description, see {@linkcode Deno.chmod}.\n   *\n   * NOTE: This API currently throws on Windows\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function chmodSync(path: string | URL, mode: number): void;\n\n  /** Change owner of a regular file or directory.\n   *\n   * This functionality is not available on Windows.\n   *\n   * ```ts\n   * await Deno.chown(\"myFile.txt\", 1000, 1002);\n   * ```\n   *\n   * Requires `allow-write` permission.\n   *\n   * Throws Error (not implemented) if executed on Windows.\n   *\n   * @tags allow-write\n   * @category File System\n   *\n   * @param path path to the file\n   * @param uid user id (UID) of the new owner, or `null` for no change\n   * @param gid group id (GID) of the new owner, or `null` for no change\n   */\n  export function chown(\n    path: string | URL,\n    uid: number | null,\n    gid: number | null\n  ): Promise<void>;\n\n  /** Synchronously change owner of a regular file or directory.\n   *\n   * This functionality is not available on Windows.\n   *\n   * ```ts\n   * Deno.chownSync(\"myFile.txt\", 1000, 1002);\n   * ```\n   *\n   * Requires `allow-write` permission.\n   *\n   * Throws Error (not implemented) if executed on Windows.\n   *\n   * @tags allow-write\n   * @category File System\n   *\n   * @param path path to the file\n   * @param uid user id (UID) of the new owner, or `null` for no change\n   * @param gid group id (GID) of the new owner, or `null` for no change\n   */\n  export function chownSync(\n    path: string | URL,\n    uid: number | null,\n    gid: number | null\n  ): void;\n\n  /**\n   * Options which can be set when using {@linkcode Deno.remove} and\n   * {@linkcode Deno.removeSync}.\n   *\n   * @category File System */\n  export interface RemoveOptions {\n    /** If set to `true`, path will be removed even if it's a non-empty directory.\n     *\n     * @default {false} */\n    recursive?: boolean;\n  }\n\n  /** Removes the named file or directory.\n   *\n   * ```ts\n   * await Deno.remove(\"/path/to/empty_dir/or/file\");\n   * await Deno.remove(\"/path/to/populated_dir/or/file\", { recursive: true });\n   * ```\n   *\n   * Throws error if permission denied, path not found, or path is a non-empty\n   * directory and the `recursive` option isn't set to `true`.\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function remove(\n    path: string | URL,\n    options?: RemoveOptions\n  ): Promise<void>;\n\n  /** Synchronously removes the named file or directory.\n   *\n   * ```ts\n   * Deno.removeSync(\"/path/to/empty_dir/or/file\");\n   * Deno.removeSync(\"/path/to/populated_dir/or/file\", { recursive: true });\n   * ```\n   *\n   * Throws error if permission denied, path not found, or path is a non-empty\n   * directory and the `recursive` option isn't set to `true`.\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function removeSync(path: string | URL, options?: RemoveOptions): void;\n\n  /** Synchronously renames (moves) `oldpath` to `newpath`. Paths may be files or\n   * directories. If `newpath` already exists and is not a directory,\n   * `renameSync()` replaces it. OS-specific restrictions may apply when\n   * `oldpath` and `newpath` are in different directories.\n   *\n   * ```ts\n   * Deno.renameSync(\"old/path\", \"new/path\");\n   * ```\n   *\n   * On Unix-like OSes, this operation does not follow symlinks at either path.\n   *\n   * It varies between platforms when the operation throws errors, and if so what\n   * they are. It's always an error to rename anything to a non-empty directory.\n   *\n   * Requires `allow-read` and `allow-write` permissions.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function renameSync(\n    oldpath: string | URL,\n    newpath: string | URL\n  ): void;\n\n  /** Renames (moves) `oldpath` to `newpath`. Paths may be files or directories.\n   * If `newpath` already exists and is not a directory, `rename()` replaces it.\n   * OS-specific restrictions may apply when `oldpath` and `newpath` are in\n   * different directories.\n   *\n   * ```ts\n   * await Deno.rename(\"old/path\", \"new/path\");\n   * ```\n   *\n   * On Unix-like OSes, this operation does not follow symlinks at either path.\n   *\n   * It varies between platforms when the operation throws errors, and if so\n   * what they are. It's always an error to rename anything to a non-empty\n   * directory.\n   *\n   * Requires `allow-read` and `allow-write` permissions.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function rename(\n    oldpath: string | URL,\n    newpath: string | URL\n  ): Promise<void>;\n\n  /** Asynchronously reads and returns the entire contents of a file as an UTF-8\n   *  decoded string. Reading a directory throws an error.\n   *\n   * ```ts\n   * const data = await Deno.readTextFile(\"hello.txt\");\n   * console.log(data);\n   * ```\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function readTextFile(\n    path: string | URL,\n    options?: ReadFileOptions\n  ): Promise<string>;\n\n  /** Synchronously reads and returns the entire contents of a file as an UTF-8\n   *  decoded string. Reading a directory throws an error.\n   *\n   * ```ts\n   * const data = Deno.readTextFileSync(\"hello.txt\");\n   * console.log(data);\n   * ```\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function readTextFileSync(path: string | URL): string;\n\n  /** Reads and resolves to the entire contents of a file as an array of bytes.\n   * `TextDecoder` can be used to transform the bytes to string if required.\n   * Reading a directory returns an empty data array.\n   *\n   * ```ts\n   * const decoder = new TextDecoder(\"utf-8\");\n   * const data = await Deno.readFile(\"hello.txt\");\n   * console.log(decoder.decode(data));\n   * ```\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function readFile(\n    path: string | URL,\n    options?: ReadFileOptions\n  ): Promise<Uint8Array>;\n\n  /** Synchronously reads and returns the entire contents of a file as an array\n   * of bytes. `TextDecoder` can be used to transform the bytes to string if\n   * required. Reading a directory returns an empty data array.\n   *\n   * ```ts\n   * const decoder = new TextDecoder(\"utf-8\");\n   * const data = Deno.readFileSync(\"hello.txt\");\n   * console.log(decoder.decode(data));\n   * ```\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function readFileSync(path: string | URL): Uint8Array;\n\n  /** Provides information about a file and is returned by\n   * {@linkcode Deno.stat}, {@linkcode Deno.lstat}, {@linkcode Deno.statSync},\n   * and {@linkcode Deno.lstatSync} or from calling `stat()` and `statSync()`\n   * on an {@linkcode Deno.FsFile} instance.\n   *\n   * @category File System\n   */\n  export interface FileInfo {\n    /** True if this is info for a regular file. Mutually exclusive to\n     * `FileInfo.isDirectory` and `FileInfo.isSymlink`. */\n    isFile: boolean;\n    /** True if this is info for a regular directory. Mutually exclusive to\n     * `FileInfo.isFile` and `FileInfo.isSymlink`. */\n    isDirectory: boolean;\n    /** True if this is info for a symlink. Mutually exclusive to\n     * `FileInfo.isFile` and `FileInfo.isDirectory`. */\n    isSymlink: boolean;\n    /** The size of the file, in bytes. */\n    size: number;\n    /** The last modification time of the file. This corresponds to the `mtime`\n     * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This\n     * may not be available on all platforms. */\n    mtime: Date | null;\n    /** The last access time of the file. This corresponds to the `atime`\n     * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not\n     * be available on all platforms. */\n    atime: Date | null;\n    /** The creation time of the file. This corresponds to the `birthtime`\n     * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may\n     * not be available on all platforms. */\n    birthtime: Date | null;\n    /** ID of the device containing the file. */\n    dev: number;\n    /** Inode number.\n     *\n     * _Linux/Mac OS only._ */\n    ino: number | null;\n    /** The underlying raw `st_mode` bits that contain the standard Unix\n     * permissions for this file/directory.\n     *\n     * _Linux/Mac OS only._ */\n    mode: number | null;\n    /** Number of hard links pointing to this file.\n     *\n     * _Linux/Mac OS only._ */\n    nlink: number | null;\n    /** User ID of the owner of this file.\n     *\n     * _Linux/Mac OS only._ */\n    uid: number | null;\n    /** Group ID of the owner of this file.\n     *\n     * _Linux/Mac OS only._ */\n    gid: number | null;\n    /** Device ID of this file.\n     *\n     * _Linux/Mac OS only._ */\n    rdev: number | null;\n    /** Blocksize for filesystem I/O.\n     *\n     * _Linux/Mac OS only._ */\n    blksize: number | null;\n    /** Number of blocks allocated to the file, in 512-byte units.\n     *\n     * _Linux/Mac OS only._ */\n    blocks: number | null;\n    /**  True if this is info for a block device.\n     *\n     * _Linux/Mac OS only._ */\n    isBlockDevice: boolean | null;\n    /**  True if this is info for a char device.\n     *\n     * _Linux/Mac OS only._ */\n    isCharDevice: boolean | null;\n    /**  True if this is info for a fifo.\n     *\n     * _Linux/Mac OS only._ */\n    isFifo: boolean | null;\n    /**  True if this is info for a socket.\n     *\n     * _Linux/Mac OS only._ */\n    isSocket: boolean | null;\n  }\n\n  /** Resolves to the absolute normalized path, with symbolic links resolved.\n   *\n   * ```ts\n   * // e.g. given /home/alice/file.txt and current directory /home/alice\n   * await Deno.symlink(\"file.txt\", \"symlink_file.txt\");\n   * const realPath = await Deno.realPath(\"./file.txt\");\n   * const realSymLinkPath = await Deno.realPath(\"./symlink_file.txt\");\n   * console.log(realPath);  // outputs \"/home/alice/file.txt\"\n   * console.log(realSymLinkPath);  // outputs \"/home/alice/file.txt\"\n   * ```\n   *\n   * Requires `allow-read` permission for the target path.\n   *\n   * Also requires `allow-read` permission for the `CWD` if the target path is\n   * relative.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function realPath(path: string | URL): Promise<string>;\n\n  /** Synchronously returns absolute normalized path, with symbolic links\n   * resolved.\n   *\n   * ```ts\n   * // e.g. given /home/alice/file.txt and current directory /home/alice\n   * Deno.symlinkSync(\"file.txt\", \"symlink_file.txt\");\n   * const realPath = Deno.realPathSync(\"./file.txt\");\n   * const realSymLinkPath = Deno.realPathSync(\"./symlink_file.txt\");\n   * console.log(realPath);  // outputs \"/home/alice/file.txt\"\n   * console.log(realSymLinkPath);  // outputs \"/home/alice/file.txt\"\n   * ```\n   *\n   * Requires `allow-read` permission for the target path.\n   *\n   * Also requires `allow-read` permission for the `CWD` if the target path is\n   * relative.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function realPathSync(path: string | URL): string;\n\n  /**\n   * Information about a directory entry returned from {@linkcode Deno.readDir}\n   * and {@linkcode Deno.readDirSync}.\n   *\n   * @category File System */\n  export interface DirEntry {\n    /** The file name of the entry. It is just the entity name and does not\n     * include the full path. */\n    name: string;\n    /** True if this is info for a regular file. Mutually exclusive to\n     * `DirEntry.isDirectory` and `DirEntry.isSymlink`. */\n    isFile: boolean;\n    /** True if this is info for a regular directory. Mutually exclusive to\n     * `DirEntry.isFile` and `DirEntry.isSymlink`. */\n    isDirectory: boolean;\n    /** True if this is info for a symlink. Mutually exclusive to\n     * `DirEntry.isFile` and `DirEntry.isDirectory`. */\n    isSymlink: boolean;\n  }\n\n  /** Reads the directory given by `path` and returns an async iterable of\n   * {@linkcode Deno.DirEntry}. The order of entries is not guaranteed.\n   *\n   * ```ts\n   * for await (const dirEntry of Deno.readDir(\"/\")) {\n   *   console.log(dirEntry.name);\n   * }\n   * ```\n   *\n   * Throws error if `path` is not a directory.\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function readDir(path: string | URL): AsyncIterable<DirEntry>;\n\n  /** Synchronously reads the directory given by `path` and returns an iterable\n   * of {@linkcode Deno.DirEntry}. The order of entries is not guaranteed.\n   *\n   * ```ts\n   * for (const dirEntry of Deno.readDirSync(\"/\")) {\n   *   console.log(dirEntry.name);\n   * }\n   * ```\n   *\n   * Throws error if `path` is not a directory.\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function readDirSync(path: string | URL): Iterable<DirEntry>;\n\n  /** Copies the contents and permissions of one file to another specified path,\n   * by default creating a new file if needed, else overwriting. Fails if target\n   * path is a directory or is unwritable.\n   *\n   * ```ts\n   * await Deno.copyFile(\"from.txt\", \"to.txt\");\n   * ```\n   *\n   * Requires `allow-read` permission on `fromPath`.\n   *\n   * Requires `allow-write` permission on `toPath`.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function copyFile(\n    fromPath: string | URL,\n    toPath: string | URL\n  ): Promise<void>;\n\n  /** Synchronously copies the contents and permissions of one file to another\n   * specified path, by default creating a new file if needed, else overwriting.\n   * Fails if target path is a directory or is unwritable.\n   *\n   * ```ts\n   * Deno.copyFileSync(\"from.txt\", \"to.txt\");\n   * ```\n   *\n   * Requires `allow-read` permission on `fromPath`.\n   *\n   * Requires `allow-write` permission on `toPath`.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function copyFileSync(\n    fromPath: string | URL,\n    toPath: string | URL\n  ): void;\n\n  /** Resolves to the full path destination of the named symbolic link.\n   *\n   * ```ts\n   * await Deno.symlink(\"./test.txt\", \"./test_link.txt\");\n   * const target = await Deno.readLink(\"./test_link.txt\"); // full path of ./test.txt\n   * ```\n   *\n   * Throws TypeError if called with a hard link.\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function readLink(path: string | URL): Promise<string>;\n\n  /** Synchronously returns the full path destination of the named symbolic\n   * link.\n   *\n   * ```ts\n   * Deno.symlinkSync(\"./test.txt\", \"./test_link.txt\");\n   * const target = Deno.readLinkSync(\"./test_link.txt\"); // full path of ./test.txt\n   * ```\n   *\n   * Throws TypeError if called with a hard link.\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function readLinkSync(path: string | URL): string;\n\n  /** Resolves to a {@linkcode Deno.FileInfo} for the specified `path`. If\n   * `path` is a symlink, information for the symlink will be returned instead\n   * of what it points to.\n   *\n   * ```ts\n   * import { assert } from \"jsr:@std/assert\";\n   * const fileInfo = await Deno.lstat(\"hello.txt\");\n   * assert(fileInfo.isFile);\n   * ```\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function lstat(path: string | URL): Promise<FileInfo>;\n\n  /** Synchronously returns a {@linkcode Deno.FileInfo} for the specified\n   * `path`. If `path` is a symlink, information for the symlink will be\n   * returned instead of what it points to.\n   *\n   * ```ts\n   * import { assert } from \"jsr:@std/assert\";\n   * const fileInfo = Deno.lstatSync(\"hello.txt\");\n   * assert(fileInfo.isFile);\n   * ```\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function lstatSync(path: string | URL): FileInfo;\n\n  /** Resolves to a {@linkcode Deno.FileInfo} for the specified `path`. Will\n   * always follow symlinks.\n   *\n   * ```ts\n   * import { assert } from \"jsr:@std/assert\";\n   * const fileInfo = await Deno.stat(\"hello.txt\");\n   * assert(fileInfo.isFile);\n   * ```\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function stat(path: string | URL): Promise<FileInfo>;\n\n  /** Synchronously returns a {@linkcode Deno.FileInfo} for the specified\n   * `path`. Will always follow symlinks.\n   *\n   * ```ts\n   * import { assert } from \"jsr:@std/assert\";\n   * const fileInfo = Deno.statSync(\"hello.txt\");\n   * assert(fileInfo.isFile);\n   * ```\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function statSync(path: string | URL): FileInfo;\n\n  /** Options for writing to a file.\n   *\n   * @category File System\n   */\n  export interface WriteFileOptions {\n    /** If set to `true`, will append to a file instead of overwriting previous\n     * contents.\n     *\n     * @default {false} */\n    append?: boolean;\n    /** Sets the option to allow creating a new file, if one doesn't already\n     * exist at the specified path.\n     *\n     * @default {true} */\n    create?: boolean;\n    /** If set to `true`, no file, directory, or symlink is allowed to exist at\n     * the target location. When createNew is set to `true`, `create` is ignored.\n     *\n     * @default {false} */\n    createNew?: boolean;\n    /** Permissions always applied to file. */\n    mode?: number;\n    /** An abort signal to allow cancellation of the file write operation.\n     *\n     * If the signal becomes aborted the write file operation will be stopped\n     * and the promise returned will be rejected with an {@linkcode AbortError}.\n     */\n    signal?: AbortSignal;\n  }\n\n  /** Write `data` to the given `path`, by default creating a new file if\n   * needed, else overwriting.\n   *\n   * ```ts\n   * const encoder = new TextEncoder();\n   * const data = encoder.encode(\"Hello world\\n\");\n   * await Deno.writeFile(\"hello1.txt\", data);  // overwrite \"hello1.txt\" or create it\n   * await Deno.writeFile(\"hello2.txt\", data, { create: false });  // only works if \"hello2.txt\" exists\n   * await Deno.writeFile(\"hello3.txt\", data, { mode: 0o777 });  // set permissions on new file\n   * await Deno.writeFile(\"hello4.txt\", data, { append: true });  // add data to the end of the file\n   * ```\n   *\n   * Requires `allow-write` permission, and `allow-read` if `options.create` is\n   * `false`.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function writeFile(\n    path: string | URL,\n    data: Uint8Array | ReadableStream<Uint8Array>,\n    options?: WriteFileOptions\n  ): Promise<void>;\n\n  /** Synchronously write `data` to the given `path`, by default creating a new\n   * file if needed, else overwriting.\n   *\n   * ```ts\n   * const encoder = new TextEncoder();\n   * const data = encoder.encode(\"Hello world\\n\");\n   * Deno.writeFileSync(\"hello1.txt\", data);  // overwrite \"hello1.txt\" or create it\n   * Deno.writeFileSync(\"hello2.txt\", data, { create: false });  // only works if \"hello2.txt\" exists\n   * Deno.writeFileSync(\"hello3.txt\", data, { mode: 0o777 });  // set permissions on new file\n   * Deno.writeFileSync(\"hello4.txt\", data, { append: true });  // add data to the end of the file\n   * ```\n   *\n   * Requires `allow-write` permission, and `allow-read` if `options.create` is\n   * `false`.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function writeFileSync(\n    path: string | URL,\n    data: Uint8Array,\n    options?: WriteFileOptions\n  ): void;\n\n  /** Write string `data` to the given `path`, by default creating a new file if\n   * needed, else overwriting.\n   *\n   * ```ts\n   * await Deno.writeTextFile(\"hello1.txt\", \"Hello world\\n\");  // overwrite \"hello1.txt\" or create it\n   * ```\n   *\n   * Requires `allow-write` permission, and `allow-read` if `options.create` is\n   * `false`.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function writeTextFile(\n    path: string | URL,\n    data: string | ReadableStream<string>,\n    options?: WriteFileOptions\n  ): Promise<void>;\n\n  /** Synchronously write string `data` to the given `path`, by default creating\n   * a new file if needed, else overwriting.\n   *\n   * ```ts\n   * Deno.writeTextFileSync(\"hello1.txt\", \"Hello world\\n\");  // overwrite \"hello1.txt\" or create it\n   * ```\n   *\n   * Requires `allow-write` permission, and `allow-read` if `options.create` is\n   * `false`.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function writeTextFileSync(\n    path: string | URL,\n    data: string,\n    options?: WriteFileOptions\n  ): void;\n\n  /** Truncates (or extends) the specified file, to reach the specified `len`.\n   * If `len` is not specified then the entire file contents are truncated.\n   *\n   * ### Truncate the entire file\n   * ```ts\n   * await Deno.truncate(\"my_file.txt\");\n   * ```\n   *\n   * ### Truncate part of the file\n   *\n   * ```ts\n   * const file = await Deno.makeTempFile();\n   * await Deno.writeTextFile(file, \"Hello World\");\n   * await Deno.truncate(file, 7);\n   * const data = await Deno.readFile(file);\n   * console.log(new TextDecoder().decode(data));  // \"Hello W\"\n   * ```\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function truncate(name: string, len?: number): Promise<void>;\n\n  /** Synchronously truncates (or extends) the specified file, to reach the\n   * specified `len`. If `len` is not specified then the entire file contents\n   * are truncated.\n   *\n   * ### Truncate the entire file\n   *\n   * ```ts\n   * Deno.truncateSync(\"my_file.txt\");\n   * ```\n   *\n   * ### Truncate part of the file\n   *\n   * ```ts\n   * const file = Deno.makeTempFileSync();\n   * Deno.writeFileSync(file, new TextEncoder().encode(\"Hello World\"));\n   * Deno.truncateSync(file, 7);\n   * const data = Deno.readFileSync(file);\n   * console.log(new TextDecoder().decode(data));\n   * ```\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function truncateSync(name: string, len?: number): void;\n\n  /** @category Runtime\n   *\n   * @deprecated This will be removed in Deno 2.0.\n   */\n  export interface OpMetrics {\n    opsDispatched: number;\n    opsDispatchedSync: number;\n    opsDispatchedAsync: number;\n    opsDispatchedAsyncUnref: number;\n    opsCompleted: number;\n    opsCompletedSync: number;\n    opsCompletedAsync: number;\n    opsCompletedAsyncUnref: number;\n    bytesSentControl: number;\n    bytesSentData: number;\n    bytesReceived: number;\n  }\n\n  /**\n   * Additional information for FsEvent objects with the \"other\" kind.\n   *\n   * - `\"rescan\"`: rescan notices indicate either a lapse in the events or a\n   *    change in the filesystem such that events received so far can no longer\n   *    be relied on to represent the state of the filesystem now. An\n   *    application that simply reacts to file changes may not care about this.\n   *    An application that keeps an in-memory representation of the filesystem\n   *    will need to care, and will need to refresh that representation directly\n   *    from the filesystem.\n   *\n   * @category File System\n   */\n  export type FsEventFlag = \"rescan\";\n\n  /**\n   * Represents a unique file system event yielded by a\n   * {@linkcode Deno.FsWatcher}.\n   *\n   * @category File System */\n  export interface FsEvent {\n    /** The kind/type of the file system event. */\n    kind:\n      | \"any\"\n      | \"access\"\n      | \"create\"\n      | \"modify\"\n      | \"rename\"\n      | \"remove\"\n      | \"other\";\n    /** An array of paths that are associated with the file system event. */\n    paths: string[];\n    /** Any additional flags associated with the event. */\n    flag?: FsEventFlag;\n  }\n\n  /**\n   * Returned by {@linkcode Deno.watchFs}. It is an async iterator yielding up\n   * system events. To stop watching the file system by calling `.close()`\n   * method.\n   *\n   * @category File System\n   */\n  export interface FsWatcher extends AsyncIterable<FsEvent>, Disposable {\n    /** Stops watching the file system and closes the watcher resource. */\n    close(): void;\n    /**\n     * Stops watching the file system and closes the watcher resource.\n     */\n    return?(value?: any): Promise<IteratorResult<FsEvent>>;\n    [Symbol.asyncIterator](): AsyncIterableIterator<FsEvent>;\n  }\n\n  /** Watch for file system events against one or more `paths`, which can be\n   * files or directories. These paths must exist already. One user action (e.g.\n   * `touch test.file`) can generate multiple file system events. Likewise,\n   * one user action can result in multiple file paths in one event (e.g. `mv\n   * old_name.txt new_name.txt`).\n   *\n   * The recursive option is `true` by default and, for directories, will watch\n   * the specified directory and all sub directories.\n   *\n   * Note that the exact ordering of the events can vary between operating\n   * systems.\n   *\n   * ```ts\n   * const watcher = Deno.watchFs(\"/\");\n   * for await (const event of watcher) {\n   *    console.log(\">>>> event\", event);\n   *    // { kind: \"create\", paths: [ \"/foo.txt\" ] }\n   * }\n   * ```\n   *\n   * Call `watcher.close()` to stop watching.\n   *\n   * ```ts\n   * const watcher = Deno.watchFs(\"/\");\n   *\n   * setTimeout(() => {\n   *   watcher.close();\n   * }, 5000);\n   *\n   * for await (const event of watcher) {\n   *    console.log(\">>>> event\", event);\n   * }\n   * ```\n   *\n   * Requires `allow-read` permission.\n   *\n   * @tags allow-read\n   * @category File System\n   */\n  export function watchFs(\n    paths: string | string[],\n    options?: { recursive: boolean }\n  ): FsWatcher;\n\n  /** Operating signals which can be listened for or sent to sub-processes. What\n   * signals and what their standard behaviors are OS dependent.\n   *\n   * @category Runtime */\n  export type Signal =\n    | \"SIGABRT\"\n    | \"SIGALRM\"\n    | \"SIGBREAK\"\n    | \"SIGBUS\"\n    | \"SIGCHLD\"\n    | \"SIGCONT\"\n    | \"SIGEMT\"\n    | \"SIGFPE\"\n    | \"SIGHUP\"\n    | \"SIGILL\"\n    | \"SIGINFO\"\n    | \"SIGINT\"\n    | \"SIGIO\"\n    | \"SIGPOLL\"\n    | \"SIGUNUSED\"\n    | \"SIGKILL\"\n    | \"SIGPIPE\"\n    | \"SIGPROF\"\n    | \"SIGPWR\"\n    | \"SIGQUIT\"\n    | \"SIGSEGV\"\n    | \"SIGSTKFLT\"\n    | \"SIGSTOP\"\n    | \"SIGSYS\"\n    | \"SIGTERM\"\n    | \"SIGTRAP\"\n    | \"SIGTSTP\"\n    | \"SIGTTIN\"\n    | \"SIGTTOU\"\n    | \"SIGURG\"\n    | \"SIGUSR1\"\n    | \"SIGUSR2\"\n    | \"SIGVTALRM\"\n    | \"SIGWINCH\"\n    | \"SIGXCPU\"\n    | \"SIGXFSZ\";\n\n  /** Registers the given function as a listener of the given signal event.\n   *\n   * ```ts\n   * Deno.addSignalListener(\n   *   \"SIGTERM\",\n   *   () => {\n   *     console.log(\"SIGTERM!\")\n   *   }\n   * );\n   * ```\n   *\n   * _Note_: On Windows only `\"SIGINT\"` (CTRL+C) and `\"SIGBREAK\"` (CTRL+Break)\n   * are supported.\n   *\n   * @category Runtime\n   */\n  export function addSignalListener(signal: Signal, handler: () => void): void;\n\n  /** Removes the given signal listener that has been registered with\n   * {@linkcode Deno.addSignalListener}.\n   *\n   * ```ts\n   * const listener = () => {\n   *   console.log(\"SIGTERM!\")\n   * };\n   * Deno.addSignalListener(\"SIGTERM\", listener);\n   * Deno.removeSignalListener(\"SIGTERM\", listener);\n   * ```\n   *\n   * _Note_: On Windows only `\"SIGINT\"` (CTRL+C) and `\"SIGBREAK\"` (CTRL+Break)\n   * are supported.\n   *\n   * @category Runtime\n   */\n  export function removeSignalListener(\n    signal: Signal,\n    handler: () => void\n  ): void;\n\n  /** Create a child process.\n   *\n   * If any stdio options are not set to `\"piped\"`, accessing the corresponding\n   * field on the `Command` or its `CommandOutput` will throw a `TypeError`.\n   *\n   * If `stdin` is set to `\"piped\"`, the `stdin` {@linkcode WritableStream}\n   * needs to be closed manually.\n   *\n   * `Command` acts as a builder. Each call to {@linkcode Command.spawn} or\n   * {@linkcode Command.output} will spawn a new subprocess.\n   *\n   * @example Spawn a subprocess and pipe the output to a file\n   *\n   * ```ts\n   * const command = new Deno.Command(Deno.execPath(), {\n   *   args: [\n   *     \"eval\",\n   *     \"console.log('Hello World')\",\n   *   ],\n   *   stdin: \"piped\",\n   *   stdout: \"piped\",\n   * });\n   * const child = command.spawn();\n   *\n   * // open a file and pipe the subprocess output to it.\n   * child.stdout.pipeTo(\n   *   Deno.openSync(\"output\", { write: true, create: true }).writable,\n   * );\n   *\n   * // manually close stdin\n   * child.stdin.close();\n   * const status = await child.status;\n   * ```\n   *\n   * @example Spawn a subprocess and collect its output\n   *\n   * ```ts\n   * const command = new Deno.Command(Deno.execPath(), {\n   *   args: [\n   *     \"eval\",\n   *     \"console.log('hello'); console.error('world')\",\n   *   ],\n   * });\n   * const { code, stdout, stderr } = await command.output();\n   * console.assert(code === 0);\n   * console.assert(\"hello\\n\" === new TextDecoder().decode(stdout));\n   * console.assert(\"world\\n\" === new TextDecoder().decode(stderr));\n   * ```\n   *\n   * @example Spawn a subprocess and collect its output synchronously\n   *\n   * ```ts\n   * const command = new Deno.Command(Deno.execPath(), {\n   *   args: [\n   *     \"eval\",\n   *     \"console.log('hello'); console.error('world')\",\n   *   ],\n   * });\n   * const { code, stdout, stderr } = command.outputSync();\n   * console.assert(code === 0);\n   * console.assert(\"hello\\n\" === new TextDecoder().decode(stdout));\n   * console.assert(\"world\\n\" === new TextDecoder().decode(stderr));\n   * ```\n   *\n   * @tags allow-run\n   * @category Subprocess\n   */\n  export class Command {\n    constructor(command: string | URL, options?: CommandOptions);\n    /**\n     * Executes the {@linkcode Deno.Command}, waiting for it to finish and\n     * collecting all of its output.\n     *\n     * Will throw an error if `stdin: \"piped\"` is set.\n     *\n     * If options `stdout` or `stderr` are not set to `\"piped\"`, accessing the\n     * corresponding field on {@linkcode Deno.CommandOutput} will throw a `TypeError`.\n     */\n    output(): Promise<CommandOutput>;\n    /**\n     * Synchronously executes the {@linkcode Deno.Command}, waiting for it to\n     * finish and collecting all of its output.\n     *\n     * Will throw an error if `stdin: \"piped\"` is set.\n     *\n     * If options `stdout` or `stderr` are not set to `\"piped\"`, accessing the\n     * corresponding field on {@linkcode Deno.CommandOutput} will throw a `TypeError`.\n     */\n    outputSync(): CommandOutput;\n    /**\n     * Spawns a streamable subprocess, allowing to use the other methods.\n     */\n    spawn(): ChildProcess;\n  }\n\n  /**\n   * The interface for handling a child process returned from\n   * {@linkcode Deno.Command.spawn}.\n   *\n   * @category Subprocess\n   */\n  export class ChildProcess implements Disposable {\n    get stdin(): WritableStream<Uint8Array>;\n    get stdout(): ReadableStream<Uint8Array>;\n    get stderr(): ReadableStream<Uint8Array>;\n    readonly pid: number;\n    /** Get the status of the child. */\n    readonly status: Promise<CommandStatus>;\n\n    /** Waits for the child to exit completely, returning all its output and\n     * status. */\n    output(): Promise<CommandOutput>;\n    /** Kills the process with given {@linkcode Deno.Signal}.\n     *\n     * Defaults to `SIGTERM` if no signal is provided.\n     *\n     * @param [signo=\"SIGTERM\"]\n     */\n    kill(signo?: Signal): void;\n\n    /** Ensure that the status of the child process prevents the Deno process\n     * from exiting. */\n    ref(): void;\n    /** Ensure that the status of the child process does not block the Deno\n     * process from exiting. */\n    unref(): void;\n\n    [Symbol.asyncDispose](): Promise<void>;\n  }\n\n  /**\n   * Options which can be set when calling {@linkcode Deno.Command}.\n   *\n   * @category Subprocess\n   */\n  export interface CommandOptions {\n    /** Arguments to pass to the process. */\n    args?: string[];\n    /**\n     * The working directory of the process.\n     *\n     * If not specified, the `cwd` of the parent process is used.\n     */\n    cwd?: string | URL;\n    /**\n     * Clear environmental variables from parent process.\n     *\n     * Doesn't guarantee that only `env` variables are present, as the OS may\n     * set environmental variables for processes.\n     *\n     * @default {false}\n     */\n    clearEnv?: boolean;\n    /** Environmental variables to pass to the subprocess. */\n    env?: Record<string, string>;\n    /**\n     * Sets the child process’s user ID. This translates to a setuid call in the\n     * child process. Failure in the set uid call will cause the spawn to fail.\n     */\n    uid?: number;\n    /** Similar to `uid`, but sets the group ID of the child process. */\n    gid?: number;\n    /**\n     * An {@linkcode AbortSignal} that allows closing the process using the\n     * corresponding {@linkcode AbortController} by sending the process a\n     * SIGTERM signal.\n     *\n     * Not supported in {@linkcode Deno.Command.outputSync}.\n     */\n    signal?: AbortSignal;\n\n    /** How `stdin` of the spawned process should be handled.\n     *\n     * Defaults to `\"inherit\"` for `output` & `outputSync`,\n     * and `\"inherit\"` for `spawn`. */\n    stdin?: \"piped\" | \"inherit\" | \"null\";\n    /** How `stdout` of the spawned process should be handled.\n     *\n     * Defaults to `\"piped\"` for `output` & `outputSync`,\n     * and `\"inherit\"` for `spawn`. */\n    stdout?: \"piped\" | \"inherit\" | \"null\";\n    /** How `stderr` of the spawned process should be handled.\n     *\n     * Defaults to `\"piped\"` for `output` & `outputSync`,\n     * and `\"inherit\"` for `spawn`. */\n    stderr?: \"piped\" | \"inherit\" | \"null\";\n\n    /** Skips quoting and escaping of the arguments on windows. This option\n     * is ignored on non-windows platforms.\n     *\n     * @default {false} */\n    windowsRawArguments?: boolean;\n  }\n\n  /**\n   * @category Subprocess\n   */\n  export interface CommandStatus {\n    /** If the child process exits with a 0 status code, `success` will be set\n     * to `true`, otherwise `false`. */\n    success: boolean;\n    /** The exit code of the child process. */\n    code: number;\n    /** The signal associated with the child process. */\n    signal: Signal | null;\n  }\n\n  /**\n   * The interface returned from calling {@linkcode Deno.Command.output} or\n   * {@linkcode Deno.Command.outputSync} which represents the result of spawning the\n   * child process.\n   *\n   * @category Subprocess\n   */\n  export interface CommandOutput extends CommandStatus {\n    /** The buffered output from the child process' `stdout`. */\n    readonly stdout: Uint8Array;\n    /** The buffered output from the child process' `stderr`. */\n    readonly stderr: Uint8Array;\n  }\n\n  /** Option which can be specified when performing {@linkcode Deno.inspect}.\n   *\n   * @category I/O */\n  export interface InspectOptions {\n    /** Stylize output with ANSI colors.\n     *\n     * @default {false} */\n    colors?: boolean;\n    /** Try to fit more than one entry of a collection on the same line.\n     *\n     * @default {true} */\n    compact?: boolean;\n    /** Traversal depth for nested objects.\n     *\n     * @default {4} */\n    depth?: number;\n    /** The maximum length for an inspection to take up a single line.\n     *\n     * @default {80} */\n    breakLength?: number;\n    /** Whether or not to escape sequences.\n     *\n     * @default {true} */\n    escapeSequences?: boolean;\n    /** The maximum number of iterable entries to print.\n     *\n     * @default {100} */\n    iterableLimit?: number;\n    /** Show a Proxy's target and handler.\n     *\n     * @default {false} */\n    showProxy?: boolean;\n    /** Sort Object, Set and Map entries by key.\n     *\n     * @default {false} */\n    sorted?: boolean;\n    /** Add a trailing comma for multiline collections.\n     *\n     * @default {false} */\n    trailingComma?: boolean;\n    /** Evaluate the result of calling getters.\n     *\n     * @default {false} */\n    getters?: boolean;\n    /** Show an object's non-enumerable properties.\n     *\n     * @default {false} */\n    showHidden?: boolean;\n    /** The maximum length of a string before it is truncated with an\n     * ellipsis. */\n    strAbbreviateSize?: number;\n  }\n\n  /** Converts the input into a string that has the same format as printed by\n   * `console.log()`.\n   *\n   * ```ts\n   * const obj = {\n   *   a: 10,\n   *   b: \"hello\",\n   * };\n   * const objAsString = Deno.inspect(obj); // { a: 10, b: \"hello\" }\n   * console.log(obj);  // prints same value as objAsString, e.g. { a: 10, b: \"hello\" }\n   * ```\n   *\n   * A custom inspect functions can be registered on objects, via the symbol\n   * `Symbol.for(\"Deno.customInspect\")`, to control and customize the output\n   * of `inspect()` or when using `console` logging:\n   *\n   * ```ts\n   * class A {\n   *   x = 10;\n   *   y = \"hello\";\n   *   [Symbol.for(\"Deno.customInspect\")]() {\n   *     return `x=${this.x}, y=${this.y}`;\n   *   }\n   * }\n   *\n   * const inStringFormat = Deno.inspect(new A()); // \"x=10, y=hello\"\n   * console.log(inStringFormat);  // prints \"x=10, y=hello\"\n   * ```\n   *\n   * A depth can be specified by using the `depth` option:\n   *\n   * ```ts\n   * Deno.inspect({a: {b: {c: {d: 'hello'}}}}, {depth: 2}); // { a: { b: [Object] } }\n   * ```\n   *\n   * @category I/O\n   */\n  export function inspect(value: unknown, options?: InspectOptions): string;\n\n  /** The name of a privileged feature which needs permission.\n   *\n   * @category Permissions\n   */\n  export type PermissionName =\n    | \"run\"\n    | \"read\"\n    | \"write\"\n    | \"net\"\n    | \"env\"\n    | \"sys\"\n    | \"ffi\";\n\n  /** The current status of the permission:\n   *\n   * - `\"granted\"` - the permission has been granted.\n   * - `\"denied\"` - the permission has been explicitly denied.\n   * - `\"prompt\"` - the permission has not explicitly granted nor denied.\n   *\n   * @category Permissions\n   */\n  export type PermissionState = \"granted\" | \"denied\" | \"prompt\";\n\n  /** The permission descriptor for the `allow-run` and `deny-run` permissions, which controls\n   * access to what sub-processes can be executed by Deno. The option `command`\n   * allows scoping the permission to a specific executable.\n   *\n   * **Warning, in practice, `allow-run` is effectively the same as `allow-all`\n   * in the sense that malicious code could execute any arbitrary code on the\n   * host.**\n   *\n   * @category Permissions */\n  export interface RunPermissionDescriptor {\n    name: \"run\";\n    /** An `allow-run` or `deny-run` permission can be scoped to a specific executable,\n     * which would be relative to the start-up CWD of the Deno CLI. */\n    command?: string | URL;\n  }\n\n  /** The permission descriptor for the `allow-read` and `deny-read` permissions, which controls\n   * access to reading resources from the local host. The option `path` allows\n   * scoping the permission to a specific path (and if the path is a directory\n   * any sub paths).\n   *\n   * Permission granted under `allow-read` only allows runtime code to attempt\n   * to read, the underlying operating system may apply additional permissions.\n   *\n   * @category Permissions */\n  export interface ReadPermissionDescriptor {\n    name: \"read\";\n    /** An `allow-read` or `deny-read` permission can be scoped to a specific path (and if\n     * the path is a directory, any sub paths). */\n    path?: string | URL;\n  }\n\n  /** The permission descriptor for the `allow-write` and `deny-write` permissions, which\n   * controls access to writing to resources from the local host. The option\n   * `path` allow scoping the permission to a specific path (and if the path is\n   * a directory any sub paths).\n   *\n   * Permission granted under `allow-write` only allows runtime code to attempt\n   * to write, the underlying operating system may apply additional permissions.\n   *\n   * @category Permissions */\n  export interface WritePermissionDescriptor {\n    name: \"write\";\n    /** An `allow-write` or `deny-write` permission can be scoped to a specific path (and if\n     * the path is a directory, any sub paths). */\n    path?: string | URL;\n  }\n\n  /** The permission descriptor for the `allow-net` and `deny-net` permissions, which controls\n   * access to opening network ports and connecting to remote hosts via the\n   * network. The option `host` allows scoping the permission for outbound\n   * connection to a specific host and port.\n   *\n   * @category Permissions */\n  export interface NetPermissionDescriptor {\n    name: \"net\";\n    /** Optional host string of the form `\"<hostname>[:<port>]\"`. Examples:\n     *\n     *      \"github.com\"\n     *      \"deno.land:8080\"\n     */\n    host?: string;\n  }\n\n  /** The permission descriptor for the `allow-env` and `deny-env` permissions, which controls\n   * access to being able to read and write to the process environment variables\n   * as well as access other information about the environment. The option\n   * `variable` allows scoping the permission to a specific environment\n   * variable.\n   *\n   * @category Permissions */\n  export interface EnvPermissionDescriptor {\n    name: \"env\";\n    /** Optional environment variable name (e.g. `PATH`). */\n    variable?: string;\n  }\n\n  /** The permission descriptor for the `allow-sys` and `deny-sys` permissions, which controls\n   * access to sensitive host system information, which malicious code might\n   * attempt to exploit. The option `kind` allows scoping the permission to a\n   * specific piece of information.\n   *\n   * @category Permissions */\n  export interface SysPermissionDescriptor {\n    name: \"sys\";\n    /** The specific information to scope the permission to. */\n    kind?:\n      | \"loadavg\"\n      | \"hostname\"\n      | \"systemMemoryInfo\"\n      | \"networkInterfaces\"\n      | \"osRelease\"\n      | \"osUptime\"\n      | \"uid\"\n      | \"gid\"\n      | \"username\"\n      | \"cpus\"\n      | \"homedir\"\n      | \"statfs\"\n      | \"getPriority\"\n      | \"setPriority\";\n  }\n\n  /** The permission descriptor for the `allow-ffi` and `deny-ffi` permissions, which controls\n   * access to loading _foreign_ code and interfacing with it via the\n   * [Foreign Function Interface API](https://docs.deno.com/runtime/manual/runtime/ffi_api)\n   * available in Deno.  The option `path` allows scoping the permission to a\n   * specific path on the host.\n   *\n   * @category Permissions */\n  export interface FfiPermissionDescriptor {\n    name: \"ffi\";\n    /** Optional path on the local host to scope the permission to. */\n    path?: string | URL;\n  }\n\n  /** Permission descriptors which define a permission and can be queried,\n   * requested, or revoked.\n   *\n   * View the specifics of the individual descriptors for more information about\n   * each permission kind.\n   *\n   * @category Permissions\n   */\n  export type PermissionDescriptor =\n    | RunPermissionDescriptor\n    | ReadPermissionDescriptor\n    | WritePermissionDescriptor\n    | NetPermissionDescriptor\n    | EnvPermissionDescriptor\n    | SysPermissionDescriptor\n    | FfiPermissionDescriptor;\n\n  /** The interface which defines what event types are supported by\n   * {@linkcode PermissionStatus} instances.\n   *\n   * @category Permissions */\n  export interface PermissionStatusEventMap {\n    change: Event;\n  }\n\n  /** An {@linkcode EventTarget} returned from the {@linkcode Deno.permissions}\n   * API which can provide updates to any state changes of the permission.\n   *\n   * @category Permissions */\n  export class PermissionStatus extends EventTarget {\n    // deno-lint-ignore no-explicit-any\n    onchange: ((this: PermissionStatus, ev: Event) => any) | null;\n    readonly state: PermissionState;\n    /**\n     * Describes if permission is only granted partially, eg. an access\n     * might be granted to \"/foo\" directory, but denied for \"/foo/bar\".\n     * In such case this field will be set to `true` when querying for\n     * read permissions of \"/foo\" directory.\n     */\n    readonly partial: boolean;\n    addEventListener<K extends keyof PermissionStatusEventMap>(\n      type: K,\n      listener: (\n        this: PermissionStatus,\n        ev: PermissionStatusEventMap[K]\n      ) => any,\n      options?: boolean | AddEventListenerOptions\n    ): void;\n    addEventListener(\n      type: string,\n      listener: EventListenerOrEventListenerObject,\n      options?: boolean | AddEventListenerOptions\n    ): void;\n    removeEventListener<K extends keyof PermissionStatusEventMap>(\n      type: K,\n      listener: (\n        this: PermissionStatus,\n        ev: PermissionStatusEventMap[K]\n      ) => any,\n      options?: boolean | EventListenerOptions\n    ): void;\n    removeEventListener(\n      type: string,\n      listener: EventListenerOrEventListenerObject,\n      options?: boolean | EventListenerOptions\n    ): void;\n  }\n\n  /**\n   * Deno's permission management API.\n   *\n   * The class which provides the interface for the {@linkcode Deno.permissions}\n   * global instance and is based on the web platform\n   * [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API),\n   * though some proposed parts of the API which are useful in a server side\n   * runtime context were removed or abandoned in the web platform specification\n   * which is why it was chosen to locate it in the {@linkcode Deno} namespace\n   * instead.\n   *\n   * By default, if the `stdin`/`stdout` is TTY for the Deno CLI (meaning it can\n   * send and receive text), then the CLI will prompt the user to grant\n   * permission when an un-granted permission is requested. This behavior can\n   * be changed by using the `--no-prompt` command at startup. When prompting\n   * the CLI will request the narrowest permission possible, potentially making\n   * it annoying to the user. The permissions APIs allow the code author to\n   * request a wider set of permissions at one time in order to provide a better\n   * user experience.\n   *\n   * @category Permissions */\n  export class Permissions {\n    /** Resolves to the current status of a permission.\n     *\n     * Note, if the permission is already granted, `request()` will not prompt\n     * the user again, therefore `query()` is only necessary if you are going\n     * to react differently existing permissions without wanting to modify them\n     * or prompt the user to modify them.\n     *\n     * ```ts\n     * const status = await Deno.permissions.query({ name: \"read\", path: \"/etc\" });\n     * console.log(status.state);\n     * ```\n     */\n    query(desc: PermissionDescriptor): Promise<PermissionStatus>;\n\n    /** Returns the current status of a permission.\n     *\n     * Note, if the permission is already granted, `request()` will not prompt\n     * the user again, therefore `querySync()` is only necessary if you are going\n     * to react differently existing permissions without wanting to modify them\n     * or prompt the user to modify them.\n     *\n     * ```ts\n     * const status = Deno.permissions.querySync({ name: \"read\", path: \"/etc\" });\n     * console.log(status.state);\n     * ```\n     */\n    querySync(desc: PermissionDescriptor): PermissionStatus;\n\n    /** Revokes a permission, and resolves to the state of the permission.\n     *\n     * ```ts\n     * import { assert } from \"jsr:@std/assert\";\n     *\n     * const status = await Deno.permissions.revoke({ name: \"run\" });\n     * assert(status.state !== \"granted\")\n     * ```\n     */\n    revoke(desc: PermissionDescriptor): Promise<PermissionStatus>;\n\n    /** Revokes a permission, and returns the state of the permission.\n     *\n     * ```ts\n     * import { assert } from \"jsr:@std/assert\";\n     *\n     * const status = Deno.permissions.revokeSync({ name: \"run\" });\n     * assert(status.state !== \"granted\")\n     * ```\n     */\n    revokeSync(desc: PermissionDescriptor): PermissionStatus;\n\n    /** Requests the permission, and resolves to the state of the permission.\n     *\n     * If the permission is already granted, the user will not be prompted to\n     * grant the permission again.\n     *\n     * ```ts\n     * const status = await Deno.permissions.request({ name: \"env\" });\n     * if (status.state === \"granted\") {\n     *   console.log(\"'env' permission is granted.\");\n     * } else {\n     *   console.log(\"'env' permission is denied.\");\n     * }\n     * ```\n     */\n    request(desc: PermissionDescriptor): Promise<PermissionStatus>;\n\n    /** Requests the permission, and returns the state of the permission.\n     *\n     * If the permission is already granted, the user will not be prompted to\n     * grant the permission again.\n     *\n     * ```ts\n     * const status = Deno.permissions.requestSync({ name: \"env\" });\n     * if (status.state === \"granted\") {\n     *   console.log(\"'env' permission is granted.\");\n     * } else {\n     *   console.log(\"'env' permission is denied.\");\n     * }\n     * ```\n     */\n    requestSync(desc: PermissionDescriptor): PermissionStatus;\n  }\n\n  /** Deno's permission management API.\n   *\n   * It is a singleton instance of the {@linkcode Permissions} object and is\n   * based on the web platform\n   * [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API),\n   * though some proposed parts of the API which are useful in a server side\n   * runtime context were removed or abandoned in the web platform specification\n   * which is why it was chosen to locate it in the {@linkcode Deno} namespace\n   * instead.\n   *\n   * By default, if the `stdin`/`stdout` is TTY for the Deno CLI (meaning it can\n   * send and receive text), then the CLI will prompt the user to grant\n   * permission when an un-granted permission is requested. This behavior can\n   * be changed by using the `--no-prompt` command at startup. When prompting\n   * the CLI will request the narrowest permission possible, potentially making\n   * it annoying to the user. The permissions APIs allow the code author to\n   * request a wider set of permissions at one time in order to provide a better\n   * user experience.\n   *\n   * Requesting already granted permissions will not prompt the user and will\n   * return that the permission was granted.\n   *\n   * ### Querying\n   *\n   * ```ts\n   * const status = await Deno.permissions.query({ name: \"read\", path: \"/etc\" });\n   * console.log(status.state);\n   * ```\n   *\n   * ```ts\n   * const status = Deno.permissions.querySync({ name: \"read\", path: \"/etc\" });\n   * console.log(status.state);\n   * ```\n   *\n   * ### Revoking\n   *\n   * ```ts\n   * import { assert } from \"jsr:@std/assert\";\n   *\n   * const status = await Deno.permissions.revoke({ name: \"run\" });\n   * assert(status.state !== \"granted\")\n   * ```\n   *\n   * ```ts\n   * import { assert } from \"jsr:@std/assert\";\n   *\n   * const status = Deno.permissions.revokeSync({ name: \"run\" });\n   * assert(status.state !== \"granted\")\n   * ```\n   *\n   * ### Requesting\n   *\n   * ```ts\n   * const status = await Deno.permissions.request({ name: \"env\" });\n   * if (status.state === \"granted\") {\n   *   console.log(\"'env' permission is granted.\");\n   * } else {\n   *   console.log(\"'env' permission is denied.\");\n   * }\n   * ```\n   *\n   * ```ts\n   * const status = Deno.permissions.requestSync({ name: \"env\" });\n   * if (status.state === \"granted\") {\n   *   console.log(\"'env' permission is granted.\");\n   * } else {\n   *   console.log(\"'env' permission is denied.\");\n   * }\n   * ```\n   *\n   * @category Permissions\n   */\n  export const permissions: Permissions;\n\n  /** Information related to the build of the current Deno runtime.\n   *\n   * Users are discouraged from code branching based on this information, as\n   * assumptions about what is available in what build environment might change\n   * over time. Developers should specifically sniff out the features they\n   * intend to use.\n   *\n   * The intended use for the information is for logging and debugging purposes.\n   *\n   * @category Runtime\n   */\n  export const build: {\n    /** The [LLVM](https://llvm.org/) target triple, which is the combination\n     * of `${arch}-${vendor}-${os}` and represent the specific build target that\n     * the current runtime was built for. */\n    target: string;\n    /** Instruction set architecture that the Deno CLI was built for. */\n    arch: \"x86_64\" | \"aarch64\";\n    /** The operating system that the Deno CLI was built for. `\"darwin\"` is\n     * also known as OSX or MacOS. */\n    os:\n      | \"darwin\"\n      | \"linux\"\n      | \"android\"\n      | \"windows\"\n      | \"freebsd\"\n      | \"netbsd\"\n      | \"aix\"\n      | \"solaris\"\n      | \"illumos\";\n    /** The computer vendor that the Deno CLI was built for. */\n    vendor: string;\n    /** Optional environment flags that were set for this build of Deno CLI. */\n    env?: string;\n  };\n\n  /** Version information related to the current Deno CLI runtime environment.\n   *\n   * Users are discouraged from code branching based on this information, as\n   * assumptions about what is available in what build environment might change\n   * over time. Developers should specifically sniff out the features they\n   * intend to use.\n   *\n   * The intended use for the information is for logging and debugging purposes.\n   *\n   * @category Runtime\n   */\n  export const version: {\n    /** Deno CLI's version. For example: `\"1.26.0\"`. */\n    deno: string;\n    /** The V8 version used by Deno. For example: `\"10.7.100.0\"`.\n     *\n     * V8 is the underlying JavaScript runtime platform that Deno is built on\n     * top of. */\n    v8: string;\n    /** The TypeScript version used by Deno. For example: `\"4.8.3\"`.\n     *\n     * A version of the TypeScript type checker and language server is built-in\n     * to the Deno CLI. */\n    typescript: string;\n  };\n\n  /** Returns the script arguments to the program.\n   *\n   * Give the following command line invocation of Deno:\n   *\n   * ```sh\n   * deno run --allow-read https://examples.deno.land/command-line-arguments.ts Sushi\n   * ```\n   *\n   * Then `Deno.args` will contain:\n   *\n   * ```ts\n   * [ \"Sushi\" ]\n   * ```\n   *\n   * If you are looking for a structured way to parse arguments, there is\n   * [`parseArgs()`](https://jsr.io/@std/cli/doc/parse-args/~/parseArgs) from\n   * the Deno Standard Library.\n   *\n   * @category Runtime\n   */\n  export const args: string[];\n\n  /** The URL of the entrypoint module entered from the command-line. It\n   * requires read permission to the CWD.\n   *\n   * Also see {@linkcode ImportMeta} for other related information.\n   *\n   * @tags allow-read\n   * @category Runtime\n   */\n  export const mainModule: string;\n\n  /** Options that can be used with {@linkcode symlink} and\n   * {@linkcode symlinkSync}.\n   *\n   * @category File System */\n  export interface SymlinkOptions {\n    /** Specify the symbolic link type as file, directory or NTFS junction. This\n     * option only applies to Windows and is ignored on other operating systems. */\n    type: \"file\" | \"dir\" | \"junction\";\n  }\n\n  /**\n   * Creates `newpath` as a symbolic link to `oldpath`.\n   *\n   * The `options.type` parameter can be set to `\"file\"`, `\"dir\"` or `\"junction\"`.\n   * This argument is only available on Windows and ignored on other platforms.\n   *\n   * ```ts\n   * await Deno.symlink(\"old/name\", \"new/name\");\n   * ```\n   *\n   * Requires full `allow-read` and `allow-write` permissions.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function symlink(\n    oldpath: string | URL,\n    newpath: string | URL,\n    options?: SymlinkOptions\n  ): Promise<void>;\n\n  /**\n   * Creates `newpath` as a symbolic link to `oldpath`.\n   *\n   * The `options.type` parameter can be set to `\"file\"`, `\"dir\"` or `\"junction\"`.\n   * This argument is only available on Windows and ignored on other platforms.\n   *\n   * ```ts\n   * Deno.symlinkSync(\"old/name\", \"new/name\");\n   * ```\n   *\n   * Requires full `allow-read` and `allow-write` permissions.\n   *\n   * @tags allow-read, allow-write\n   * @category File System\n   */\n  export function symlinkSync(\n    oldpath: string | URL,\n    newpath: string | URL,\n    options?: SymlinkOptions\n  ): void;\n\n  /**\n   * Synchronously changes the access (`atime`) and modification (`mtime`) times\n   * of a file system object referenced by `path`. Given times are either in\n   * seconds (UNIX epoch time) or as `Date` objects.\n   *\n   * ```ts\n   * Deno.utimeSync(\"myfile.txt\", 1556495550, new Date());\n   * ```\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function utimeSync(\n    path: string | URL,\n    atime: number | Date,\n    mtime: number | Date\n  ): void;\n\n  /**\n   * Changes the access (`atime`) and modification (`mtime`) times of a file\n   * system object referenced by `path`. Given times are either in seconds\n   * (UNIX epoch time) or as `Date` objects.\n   *\n   * ```ts\n   * await Deno.utime(\"myfile.txt\", 1556495550, new Date());\n   * ```\n   *\n   * Requires `allow-write` permission.\n   *\n   * @tags allow-write\n   * @category File System\n   */\n  export function utime(\n    path: string | URL,\n    atime: number | Date,\n    mtime: number | Date\n  ): Promise<void>;\n\n  /** Retrieve the process umask.  If `mask` is provided, sets the process umask.\n   * This call always returns what the umask was before the call.\n   *\n   * ```ts\n   * console.log(Deno.umask());  // e.g. 18 (0o022)\n   * const prevUmaskValue = Deno.umask(0o077);  // e.g. 18 (0o022)\n   * console.log(Deno.umask());  // e.g. 63 (0o077)\n   * ```\n   *\n   * This API is under consideration to determine if permissions are required to\n   * call it.\n   *\n   * *Note*: This API is not implemented on Windows\n   *\n   * @category File System\n   */\n  export function umask(mask?: number): number;\n\n  /** The object that is returned from a {@linkcode Deno.upgradeWebSocket}\n   * request.\n   *\n   * @category Web Sockets */\n  export interface WebSocketUpgrade {\n    /** The response object that represents the HTTP response to the client,\n     * which should be used to the {@linkcode RequestEvent} `.respondWith()` for\n     * the upgrade to be successful. */\n    response: Response;\n    /** The {@linkcode WebSocket} interface to communicate to the client via a\n     * web socket. */\n    socket: WebSocket;\n  }\n\n  /** Options which can be set when performing a\n   * {@linkcode Deno.upgradeWebSocket} upgrade of a {@linkcode Request}\n   *\n   * @category Web Sockets */\n  export interface UpgradeWebSocketOptions {\n    /** Sets the `.protocol` property on the client side web socket to the\n     * value provided here, which should be one of the strings specified in the\n     * `protocols` parameter when requesting the web socket. This is intended\n     * for clients and servers to specify sub-protocols to use to communicate to\n     * each other. */\n    protocol?: string;\n    /** If the client does not respond to this frame with a\n     * `pong` within the timeout specified, the connection is deemed\n     * unhealthy and is closed. The `close` and `error` event will be emitted.\n     *\n     * The unit is seconds, with a default of 30.\n     * Set to `0` to disable timeouts. */\n    idleTimeout?: number;\n  }\n\n  /**\n   * Upgrade an incoming HTTP request to a WebSocket.\n   *\n   * Given a {@linkcode Request}, returns a pair of {@linkcode WebSocket} and\n   * {@linkcode Response} instances. The original request must be responded to\n   * with the returned response for the websocket upgrade to be successful.\n   *\n   * ```ts\n   * Deno.serve((req) => {\n   *   if (req.headers.get(\"upgrade\") !== \"websocket\") {\n   *     return new Response(null, { status: 501 });\n   *   }\n   *   const { socket, response } = Deno.upgradeWebSocket(req);\n   *   socket.addEventListener(\"open\", () => {\n   *     console.log(\"a client connected!\");\n   *   });\n   *   socket.addEventListener(\"message\", (event) => {\n   *     if (event.data === \"ping\") {\n   *       socket.send(\"pong\");\n   *     }\n   *   });\n   *   return response;\n   * });\n   * ```\n   *\n   * If the request body is disturbed (read from) before the upgrade is\n   * completed, upgrading fails.\n   *\n   * This operation does not yet consume the request or open the websocket. This\n   * only happens once the returned response has been passed to `respondWith()`.\n   *\n   * @category Web Sockets\n   */\n  export function upgradeWebSocket(\n    request: Request,\n    options?: UpgradeWebSocketOptions\n  ): WebSocketUpgrade;\n\n  /** Send a signal to process under given `pid`. The value and meaning of the\n   * `signal` to the process is operating system and process dependant.\n   * {@linkcode Signal} provides the most common signals. Default signal\n   * is `\"SIGTERM\"`.\n   *\n   * The term `kill` is adopted from the UNIX-like command line command `kill`\n   * which also signals processes.\n   *\n   * If `pid` is negative, the signal will be sent to the process group\n   * identified by `pid`. An error will be thrown if a negative `pid` is used on\n   * Windows.\n   *\n   * ```ts\n   * const command = new Deno.Command(\"sleep\", { args: [\"10000\"] });\n   * const child = command.spawn();\n   *\n   * Deno.kill(child.pid, \"SIGINT\");\n   * ```\n   *\n   * Requires `allow-run` permission.\n   *\n   * @tags allow-run\n   * @category Subprocess\n   */\n  export function kill(pid: number, signo?: Signal): void;\n\n  /** The type of the resource record to resolve via DNS using\n   * {@linkcode Deno.resolveDns}.\n   *\n   * Only the listed types are supported currently.\n   *\n   * @category Network\n   */\n  export type RecordType =\n    | \"A\"\n    | \"AAAA\"\n    | \"ANAME\"\n    | \"CAA\"\n    | \"CNAME\"\n    | \"MX\"\n    | \"NAPTR\"\n    | \"NS\"\n    | \"PTR\"\n    | \"SOA\"\n    | \"SRV\"\n    | \"TXT\";\n\n  /**\n   * Options which can be set when using {@linkcode Deno.resolveDns}.\n   *\n   * @category Network */\n  export interface ResolveDnsOptions {\n    /** The name server to be used for lookups.\n     *\n     * If not specified, defaults to the system configuration. For example\n     * `/etc/resolv.conf` on Unix-like systems. */\n    nameServer?: {\n      /** The IP address of the name server. */\n      ipAddr: string;\n      /** The port number the query will be sent to.\n       *\n       * @default {53} */\n      port?: number;\n    };\n    /**\n     * An abort signal to allow cancellation of the DNS resolution operation.\n     * If the signal becomes aborted the resolveDns operation will be stopped\n     * and the promise returned will be rejected with an AbortError.\n     */\n    signal?: AbortSignal;\n  }\n\n  /** If {@linkcode Deno.resolveDns} is called with `\"CAA\"` record type\n   * specified, it will resolve with an array of objects with this interface.\n   *\n   * @category Network\n   */\n  export interface CaaRecord {\n    /** If `true`, indicates that the corresponding property tag **must** be\n     * understood if the semantics of the CAA record are to be correctly\n     * interpreted by an issuer.\n     *\n     * Issuers **must not** issue certificates for a domain if the relevant CAA\n     * Resource Record set contains unknown property tags that have `critical`\n     * set. */\n    critical: boolean;\n    /** An string that represents the identifier of the property represented by\n     * the record. */\n    tag: string;\n    /** The value associated with the tag. */\n    value: string;\n  }\n\n  /** If {@linkcode Deno.resolveDns} is called with `\"MX\"` record type\n   * specified, it will return an array of objects with this interface.\n   *\n   * @category Network */\n  export interface MxRecord {\n    /** A priority value, which is a relative value compared to the other\n     * preferences of MX records for the domain. */\n    preference: number;\n    /** The server that mail should be delivered to. */\n    exchange: string;\n  }\n\n  /** If {@linkcode Deno.resolveDns} is called with `\"NAPTR\"` record type\n   * specified, it will return an array of objects with this interface.\n   *\n   * @category Network */\n  export interface NaptrRecord {\n    order: number;\n    preference: number;\n    flags: string;\n    services: string;\n    regexp: string;\n    replacement: string;\n  }\n\n  /** If {@linkcode Deno.resolveDns} is called with `\"SOA\"` record type\n   * specified, it will return an array of objects with this interface.\n   *\n   * @category Network */\n  export interface SoaRecord {\n    mname: string;\n    rname: string;\n    serial: number;\n    refresh: number;\n    retry: number;\n    expire: number;\n    minimum: number;\n  }\n\n  /** If {@linkcode Deno.resolveDns} is called with `\"SRV\"` record type\n   * specified, it will return an array of objects with this interface.\n   *\n   * @category Network\n   */\n  export interface SrvRecord {\n    priority: number;\n    weight: number;\n    port: number;\n    target: string;\n  }\n\n  /**\n   * Performs DNS resolution against the given query, returning resolved\n   * records.\n   *\n   * Fails in the cases such as:\n   *\n   * - the query is in invalid format.\n   * - the options have an invalid parameter. For example `nameServer.port` is\n   *   beyond the range of 16-bit unsigned integer.\n   * - the request timed out.\n   *\n   * ```ts\n   * const a = await Deno.resolveDns(\"example.com\", \"A\");\n   *\n   * const aaaa = await Deno.resolveDns(\"example.com\", \"AAAA\", {\n   *   nameServer: { ipAddr: \"8.8.8.8\", port: 53 },\n   * });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function resolveDns(\n    query: string,\n    recordType: \"A\" | \"AAAA\" | \"ANAME\" | \"CNAME\" | \"NS\" | \"PTR\",\n    options?: ResolveDnsOptions\n  ): Promise<string[]>;\n\n  /**\n   * Performs DNS resolution against the given query, returning resolved\n   * records.\n   *\n   * Fails in the cases such as:\n   *\n   * - the query is in invalid format.\n   * - the options have an invalid parameter. For example `nameServer.port` is\n   *   beyond the range of 16-bit unsigned integer.\n   * - the request timed out.\n   *\n   * ```ts\n   * const a = await Deno.resolveDns(\"example.com\", \"A\");\n   *\n   * const aaaa = await Deno.resolveDns(\"example.com\", \"AAAA\", {\n   *   nameServer: { ipAddr: \"8.8.8.8\", port: 53 },\n   * });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function resolveDns(\n    query: string,\n    recordType: \"CAA\",\n    options?: ResolveDnsOptions\n  ): Promise<CaaRecord[]>;\n\n  /**\n   * Performs DNS resolution against the given query, returning resolved\n   * records.\n   *\n   * Fails in the cases such as:\n   *\n   * - the query is in invalid format.\n   * - the options have an invalid parameter. For example `nameServer.port` is\n   *   beyond the range of 16-bit unsigned integer.\n   * - the request timed out.\n   *\n   * ```ts\n   * const a = await Deno.resolveDns(\"example.com\", \"A\");\n   *\n   * const aaaa = await Deno.resolveDns(\"example.com\", \"AAAA\", {\n   *   nameServer: { ipAddr: \"8.8.8.8\", port: 53 },\n   * });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function resolveDns(\n    query: string,\n    recordType: \"MX\",\n    options?: ResolveDnsOptions\n  ): Promise<MxRecord[]>;\n\n  /**\n   * Performs DNS resolution against the given query, returning resolved\n   * records.\n   *\n   * Fails in the cases such as:\n   *\n   * - the query is in invalid format.\n   * - the options have an invalid parameter. For example `nameServer.port` is\n   *   beyond the range of 16-bit unsigned integer.\n   * - the request timed out.\n   *\n   * ```ts\n   * const a = await Deno.resolveDns(\"example.com\", \"A\");\n   *\n   * const aaaa = await Deno.resolveDns(\"example.com\", \"AAAA\", {\n   *   nameServer: { ipAddr: \"8.8.8.8\", port: 53 },\n   * });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function resolveDns(\n    query: string,\n    recordType: \"NAPTR\",\n    options?: ResolveDnsOptions\n  ): Promise<NaptrRecord[]>;\n\n  /**\n   * Performs DNS resolution against the given query, returning resolved\n   * records.\n   *\n   * Fails in the cases such as:\n   *\n   * - the query is in invalid format.\n   * - the options have an invalid parameter. For example `nameServer.port` is\n   *   beyond the range of 16-bit unsigned integer.\n   * - the request timed out.\n   *\n   * ```ts\n   * const a = await Deno.resolveDns(\"example.com\", \"A\");\n   *\n   * const aaaa = await Deno.resolveDns(\"example.com\", \"AAAA\", {\n   *   nameServer: { ipAddr: \"8.8.8.8\", port: 53 },\n   * });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function resolveDns(\n    query: string,\n    recordType: \"SOA\",\n    options?: ResolveDnsOptions\n  ): Promise<SoaRecord[]>;\n\n  /**\n   * Performs DNS resolution against the given query, returning resolved\n   * records.\n   *\n   * Fails in the cases such as:\n   *\n   * - the query is in invalid format.\n   * - the options have an invalid parameter. For example `nameServer.port` is\n   *   beyond the range of 16-bit unsigned integer.\n   * - the request timed out.\n   *\n   * ```ts\n   * const a = await Deno.resolveDns(\"example.com\", \"A\");\n   *\n   * const aaaa = await Deno.resolveDns(\"example.com\", \"AAAA\", {\n   *   nameServer: { ipAddr: \"8.8.8.8\", port: 53 },\n   * });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function resolveDns(\n    query: string,\n    recordType: \"SRV\",\n    options?: ResolveDnsOptions\n  ): Promise<SrvRecord[]>;\n\n  /**\n   * Performs DNS resolution against the given query, returning resolved\n   * records.\n   *\n   * Fails in the cases such as:\n   *\n   * - the query is in invalid format.\n   * - the options have an invalid parameter. For example `nameServer.port` is\n   *   beyond the range of 16-bit unsigned integer.\n   * - the request timed out.\n   *\n   * ```ts\n   * const a = await Deno.resolveDns(\"example.com\", \"A\");\n   *\n   * const aaaa = await Deno.resolveDns(\"example.com\", \"AAAA\", {\n   *   nameServer: { ipAddr: \"8.8.8.8\", port: 53 },\n   * });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function resolveDns(\n    query: string,\n    recordType: \"TXT\",\n    options?: ResolveDnsOptions\n  ): Promise<string[][]>;\n\n  /**\n   * Performs DNS resolution against the given query, returning resolved\n   * records.\n   *\n   * Fails in the cases such as:\n   *\n   * - the query is in invalid format.\n   * - the options have an invalid parameter. For example `nameServer.port` is\n   *   beyond the range of 16-bit unsigned integer.\n   * - the request timed out.\n   *\n   * ```ts\n   * const a = await Deno.resolveDns(\"example.com\", \"A\");\n   *\n   * const aaaa = await Deno.resolveDns(\"example.com\", \"AAAA\", {\n   *   nameServer: { ipAddr: \"8.8.8.8\", port: 53 },\n   * });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function resolveDns(\n    query: string,\n    recordType: RecordType,\n    options?: ResolveDnsOptions\n  ): Promise<\n    | string[]\n    | CaaRecord[]\n    | MxRecord[]\n    | NaptrRecord[]\n    | SoaRecord[]\n    | SrvRecord[]\n    | string[][]\n  >;\n\n  /**\n   * Make the timer of the given `id` block the event loop from finishing.\n   *\n   * @category Runtime\n   */\n  export function refTimer(id: number): void;\n\n  /**\n   * Make the timer of the given `id` not block the event loop from finishing.\n   *\n   * @category Runtime\n   */\n  export function unrefTimer(id: number): void;\n\n  /**\n   * Returns the user id of the process on POSIX platforms. Returns null on Windows.\n   *\n   * ```ts\n   * console.log(Deno.uid());\n   * ```\n   *\n   * Requires `allow-sys` permission.\n   *\n   * @tags allow-sys\n   * @category Runtime\n   */\n  export function uid(): number | null;\n\n  /**\n   * Returns the group id of the process on POSIX platforms. Returns null on windows.\n   *\n   * ```ts\n   * console.log(Deno.gid());\n   * ```\n   *\n   * Requires `allow-sys` permission.\n   *\n   * @tags allow-sys\n   * @category Runtime\n   */\n  export function gid(): number | null;\n\n  /** Additional information for an HTTP request and its connection.\n   *\n   * @category HTTP Server\n   */\n  export interface ServeHandlerInfo<Addr extends Deno.Addr = Deno.Addr> {\n    /** The remote address of the connection. */\n    remoteAddr: Addr;\n    /** The completion promise */\n    completed: Promise<void>;\n  }\n\n  /** A handler for HTTP requests. Consumes a request and returns a response.\n   *\n   * If a handler throws, the server calling the handler will assume the impact\n   * of the error is isolated to the individual request. It will catch the error\n   * and if necessary will close the underlying connection.\n   *\n   * @category HTTP Server\n   */\n  export type ServeHandler<Addr extends Deno.Addr = Deno.Addr> = (\n    request: Request,\n    info: ServeHandlerInfo<Addr>\n  ) => Response | Promise<Response>;\n\n  /** Interface that module run with `deno serve` subcommand must conform to.\n   *\n   * To ensure your code is type-checked properly, make sure to add `satisfies Deno.ServeDefaultExport`\n   * to the `export default { ... }` like so:\n   *\n   * ```ts\n   * export default {\n   *   fetch(req) {\n   *     return new Response(\"Hello world\");\n   *   }\n   * } satisfies Deno.ServeDefaultExport;\n   * ```\n   *\n   * @category HTTP Server\n   */\n  export interface ServeDefaultExport {\n    /** A handler for HTTP requests. Consumes a request and returns a response.\n     *\n     * If a handler throws, the server calling the handler will assume the impact\n     * of the error is isolated to the individual request. It will catch the error\n     * and if necessary will close the underlying connection.\n     *\n     * @category HTTP Server\n     */\n    fetch: ServeHandler;\n  }\n\n  /** Options which can be set when calling {@linkcode Deno.serve}.\n   *\n   * @category HTTP Server\n   */\n  export interface ServeOptions<Addr extends Deno.Addr = Deno.Addr> {\n    /** An {@linkcode AbortSignal} to close the server and all connections. */\n    signal?: AbortSignal;\n\n    /** The handler to invoke when route handlers throw an error. */\n    onError?: (error: unknown) => Response | Promise<Response>;\n\n    /** The callback which is called when the server starts listening. */\n    onListen?: (localAddr: Addr) => void;\n  }\n\n  /**\n   * Options that can be passed to `Deno.serve` to create a server listening on\n   * a TCP port.\n   *\n   * @category HTTP Server\n   */\n  export interface ServeTcpOptions extends ServeOptions<Deno.NetAddr> {\n    /** The transport to use. */\n    transport?: \"tcp\";\n\n    /** The port to listen on.\n     *\n     * Set to `0` to listen on any available port.\n     *\n     * @default {8000} */\n    port?: number;\n\n    /** A literal IP address or host name that can be resolved to an IP address.\n     *\n     * __Note about `0.0.0.0`__ While listening `0.0.0.0` works on all platforms,\n     * the browsers on Windows don't work with the address `0.0.0.0`.\n     * You should show the message like `server running on localhost:8080` instead of\n     * `server running on 0.0.0.0:8080` if your program supports Windows.\n     *\n     * @default {\"0.0.0.0\"} */\n    hostname?: string;\n\n    /** Sets `SO_REUSEPORT` on POSIX systems. */\n    reusePort?: boolean;\n  }\n\n  /**\n   * Options that can be passed to `Deno.serve` to create a server listening on\n   * a Unix domain socket.\n   *\n   * @category HTTP Server\n   */\n  export interface ServeUnixOptions extends ServeOptions<Deno.UnixAddr> {\n    /** The transport to use. */\n    transport?: \"unix\";\n\n    /** The unix domain socket path to listen on. */\n    path: string;\n  }\n\n  /**\n   * @category HTTP Server\n   */\n  export interface ServeInit<Addr extends Deno.Addr = Deno.Addr> {\n    /** The handler to invoke to process each incoming request. */\n    handler: ServeHandler<Addr>;\n  }\n\n  /** An instance of the server created using `Deno.serve()` API.\n   *\n   * @category HTTP Server\n   */\n  export interface HttpServer<Addr extends Deno.Addr = Deno.Addr>\n    extends AsyncDisposable {\n    /** A promise that resolves once server finishes - eg. when aborted using\n     * the signal passed to {@linkcode ServeOptions.signal}.\n     */\n    finished: Promise<void>;\n\n    /** The local address this server is listening on. */\n    addr: Addr;\n\n    /**\n     * Make the server block the event loop from finishing.\n     *\n     * Note: the server blocks the event loop from finishing by default.\n     * This method is only meaningful after `.unref()` is called.\n     */\n    ref(): void;\n\n    /** Make the server not block the event loop from finishing. */\n    unref(): void;\n\n    /** Gracefully close the server. No more new connections will be accepted,\n     * while pending requests will be allowed to finish.\n     */\n    shutdown(): Promise<void>;\n  }\n\n  /** Serves HTTP requests with the given handler.\n   *\n   * The below example serves with the port `8000` on hostname `\"127.0.0.1\"`.\n   *\n   * ```ts\n   * Deno.serve((_req) => new Response(\"Hello, world\"));\n   * ```\n   *\n   * @category HTTP Server\n   */\n  export function serve(\n    handler: ServeHandler<Deno.NetAddr>\n  ): HttpServer<Deno.NetAddr>;\n  /** Serves HTTP requests with the given option bag and handler.\n   *\n   * You can specify the socket path with `path` option.\n   *\n   * ```ts\n   * Deno.serve(\n   *   { path: \"path/to/socket\" },\n   *   (_req) => new Response(\"Hello, world\")\n   * );\n   * ```\n   *\n   * You can stop the server with an {@linkcode AbortSignal}. The abort signal\n   * needs to be passed as the `signal` option in the options bag. The server\n   * aborts when the abort signal is aborted. To wait for the server to close,\n   * await the promise returned from the `Deno.serve` API.\n   *\n   * ```ts\n   * const ac = new AbortController();\n   *\n   * const server = Deno.serve(\n   *    { signal: ac.signal, path: \"path/to/socket\" },\n   *    (_req) => new Response(\"Hello, world\")\n   * );\n   * server.finished.then(() => console.log(\"Server closed\"));\n   *\n   * console.log(\"Closing server...\");\n   * ac.abort();\n   * ```\n   *\n   * By default `Deno.serve` prints the message\n   * `Listening on path/to/socket` on listening. If you like to\n   * change this behavior, you can specify a custom `onListen` callback.\n   *\n   * ```ts\n   * Deno.serve({\n   *   onListen({ path }) {\n   *     console.log(`Server started at ${path}`);\n   *     // ... more info specific to your server ..\n   *   },\n   *   path: \"path/to/socket\",\n   * }, (_req) => new Response(\"Hello, world\"));\n   * ```\n   *\n   * @category HTTP Server\n   */\n  export function serve(\n    options: ServeUnixOptions,\n    handler: ServeHandler<Deno.UnixAddr>\n  ): HttpServer<Deno.UnixAddr>;\n  /** Serves HTTP requests with the given option bag and handler.\n   *\n   * You can specify an object with a port and hostname option, which is the\n   * address to listen on. The default is port `8000` on hostname `\"0.0.0.0\"`.\n   *\n   * You can change the address to listen on using the `hostname` and `port`\n   * options. The below example serves on port `3000` and hostname `\"127.0.0.1\"`.\n   *\n   * ```ts\n   * Deno.serve(\n   *   { port: 3000, hostname: \"127.0.0.1\" },\n   *   (_req) => new Response(\"Hello, world\")\n   * );\n   * ```\n   *\n   * You can stop the server with an {@linkcode AbortSignal}. The abort signal\n   * needs to be passed as the `signal` option in the options bag. The server\n   * aborts when the abort signal is aborted. To wait for the server to close,\n   * await the promise returned from the `Deno.serve` API.\n   *\n   * ```ts\n   * const ac = new AbortController();\n   *\n   * const server = Deno.serve(\n   *    { signal: ac.signal },\n   *    (_req) => new Response(\"Hello, world\")\n   * );\n   * server.finished.then(() => console.log(\"Server closed\"));\n   *\n   * console.log(\"Closing server...\");\n   * ac.abort();\n   * ```\n   *\n   * By default `Deno.serve` prints the message\n   * `Listening on http://<hostname>:<port>/` on listening. If you like to\n   * change this behavior, you can specify a custom `onListen` callback.\n   *\n   * ```ts\n   * Deno.serve({\n   *   onListen({ port, hostname }) {\n   *     console.log(`Server started at http://${hostname}:${port}`);\n   *     // ... more info specific to your server ..\n   *   },\n   * }, (_req) => new Response(\"Hello, world\"));\n   * ```\n   *\n   * To enable TLS you must specify the `key` and `cert` options.\n   *\n   * ```ts\n   * const cert = \"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n\";\n   * const key = \"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\";\n   * Deno.serve({ cert, key }, (_req) => new Response(\"Hello, world\"));\n   * ```\n   *\n   * @category HTTP Server\n   */\n  export function serve(\n    options: ServeTcpOptions | (ServeTcpOptions & TlsCertifiedKeyPem),\n    handler: ServeHandler<Deno.NetAddr>\n  ): HttpServer<Deno.NetAddr>;\n  /** Serves HTTP requests with the given option bag.\n   *\n   * You can specify an object with the path option, which is the\n   * unix domain socket to listen on.\n   *\n   * ```ts\n   * const ac = new AbortController();\n   *\n   * const server = Deno.serve({\n   *   path: \"path/to/socket\",\n   *   handler: (_req) => new Response(\"Hello, world\"),\n   *   signal: ac.signal,\n   *   onListen({ path }) {\n   *     console.log(`Server started at ${path}`);\n   *   },\n   * });\n   * server.finished.then(() => console.log(\"Server closed\"));\n   *\n   * console.log(\"Closing server...\");\n   * ac.abort();\n   * ```\n   *\n   * @category HTTP Server\n   */\n  export function serve(\n    options: ServeUnixOptions & ServeInit<Deno.UnixAddr>\n  ): HttpServer<Deno.UnixAddr>;\n  /** Serves HTTP requests with the given option bag.\n   *\n   * You can specify an object with a port and hostname option, which is the\n   * address to listen on. The default is port `8000` on hostname `\"0.0.0.0\"`.\n   *\n   * ```ts\n   * const ac = new AbortController();\n   *\n   * const server = Deno.serve({\n   *   port: 3000,\n   *   hostname: \"127.0.0.1\",\n   *   handler: (_req) => new Response(\"Hello, world\"),\n   *   signal: ac.signal,\n   *   onListen({ port, hostname }) {\n   *     console.log(`Server started at http://${hostname}:${port}`);\n   *   },\n   * });\n   * server.finished.then(() => console.log(\"Server closed\"));\n   *\n   * console.log(\"Closing server...\");\n   * ac.abort();\n   * ```\n   *\n   * @category HTTP Server\n   */\n  export function serve(\n    options: (ServeTcpOptions | (ServeTcpOptions & TlsCertifiedKeyPem)) &\n      ServeInit<Deno.NetAddr>\n  ): HttpServer<Deno.NetAddr>;\n\n  /** All plain number types for interfacing with foreign functions.\n   *\n   * @category FFI\n   */\n  export type NativeNumberType =\n    | \"u8\"\n    | \"i8\"\n    | \"u16\"\n    | \"i16\"\n    | \"u32\"\n    | \"i32\"\n    | \"f32\"\n    | \"f64\";\n\n  /** All BigInt number types for interfacing with foreign functions.\n   *\n   * @category FFI\n   */\n  export type NativeBigIntType = \"u64\" | \"i64\" | \"usize\" | \"isize\";\n\n  /** The native boolean type for interfacing to foreign functions.\n   *\n   * @category FFI\n   */\n  export type NativeBooleanType = \"bool\";\n\n  /** The native pointer type for interfacing to foreign functions.\n   *\n   * @category FFI\n   */\n  export type NativePointerType = \"pointer\";\n\n  /** The native buffer type for interfacing to foreign functions.\n   *\n   * @category FFI\n   */\n  export type NativeBufferType = \"buffer\";\n\n  /** The native function type for interfacing with foreign functions.\n   *\n   * @category FFI\n   */\n  export type NativeFunctionType = \"function\";\n\n  /** The native void type for interfacing with foreign functions.\n   *\n   * @category FFI\n   */\n  export type NativeVoidType = \"void\";\n\n  /** The native struct type for interfacing with foreign functions.\n   *\n   * @category FFI\n   */\n  export interface NativeStructType {\n    readonly struct: readonly NativeType[];\n  }\n\n  /**\n   * @category FFI\n   */\n  export const brand: unique symbol;\n\n  /**\n   * @category FFI\n   */\n  export type NativeU8Enum<T extends number> = \"u8\" & { [brand]: T };\n  /**\n   * @category FFI\n   */\n  export type NativeI8Enum<T extends number> = \"i8\" & { [brand]: T };\n  /**\n   * @category FFI\n   */\n  export type NativeU16Enum<T extends number> = \"u16\" & { [brand]: T };\n  /**\n   * @category FFI\n   */\n  export type NativeI16Enum<T extends number> = \"i16\" & { [brand]: T };\n  /**\n   * @category FFI\n   */\n  export type NativeU32Enum<T extends number> = \"u32\" & { [brand]: T };\n  /**\n   * @category FFI\n   */\n  export type NativeI32Enum<T extends number> = \"i32\" & { [brand]: T };\n  /**\n   * @category FFI\n   */\n  export type NativeTypedPointer<T extends PointerObject> = \"pointer\" & {\n    [brand]: T;\n  };\n  /**\n   * @category FFI\n   */\n  export type NativeTypedFunction<T extends UnsafeCallbackDefinition> =\n    \"function\" & {\n      [brand]: T;\n    };\n\n  /** All supported types for interfacing with foreign functions.\n   *\n   * @category FFI\n   */\n  export type NativeType =\n    | NativeNumberType\n    | NativeBigIntType\n    | NativeBooleanType\n    | NativePointerType\n    | NativeBufferType\n    | NativeFunctionType\n    | NativeStructType;\n\n  /** @category FFI\n   */\n  export type NativeResultType = NativeType | NativeVoidType;\n\n  /** Type conversion for foreign symbol parameters and unsafe callback return\n   * types.\n   *\n   * @category FFI\n   */\n  export type ToNativeType<T extends NativeType = NativeType> =\n    T extends NativeStructType\n      ? BufferSource\n      : T extends NativeNumberType\n        ? T extends NativeU8Enum<infer U>\n          ? U\n          : T extends NativeI8Enum<infer U>\n            ? U\n            : T extends NativeU16Enum<infer U>\n              ? U\n              : T extends NativeI16Enum<infer U>\n                ? U\n                : T extends NativeU32Enum<infer U>\n                  ? U\n                  : T extends NativeI32Enum<infer U>\n                    ? U\n                    : number\n        : T extends NativeBigIntType\n          ? bigint\n          : T extends NativeBooleanType\n            ? boolean\n            : T extends NativePointerType\n              ? T extends NativeTypedPointer<infer U>\n                ? U | null\n                : PointerValue\n              : T extends NativeFunctionType\n                ? T extends NativeTypedFunction<infer U>\n                  ? PointerValue<U> | null\n                  : PointerValue\n                : T extends NativeBufferType\n                  ? BufferSource | null\n                  : never;\n\n  /** Type conversion for unsafe callback return types.\n   *\n   * @category FFI\n   */\n  export type ToNativeResultType<\n    T extends NativeResultType = NativeResultType,\n  > = T extends NativeStructType\n    ? BufferSource\n    : T extends NativeNumberType\n      ? T extends NativeU8Enum<infer U>\n        ? U\n        : T extends NativeI8Enum<infer U>\n          ? U\n          : T extends NativeU16Enum<infer U>\n            ? U\n            : T extends NativeI16Enum<infer U>\n              ? U\n              : T extends NativeU32Enum<infer U>\n                ? U\n                : T extends NativeI32Enum<infer U>\n                  ? U\n                  : number\n      : T extends NativeBigIntType\n        ? bigint\n        : T extends NativeBooleanType\n          ? boolean\n          : T extends NativePointerType\n            ? T extends NativeTypedPointer<infer U>\n              ? U | null\n              : PointerValue\n            : T extends NativeFunctionType\n              ? T extends NativeTypedFunction<infer U>\n                ? PointerObject<U> | null\n                : PointerValue\n              : T extends NativeBufferType\n                ? BufferSource | null\n                : T extends NativeVoidType\n                  ? void\n                  : never;\n\n  /** A utility type for conversion of parameter types of foreign functions.\n   *\n   * @category FFI\n   */\n  export type ToNativeParameterTypes<T extends readonly NativeType[]> =\n    //\n    [T[number][]] extends [T]\n      ? ToNativeType<T[number]>[]\n      : [readonly T[number][]] extends [T]\n        ? readonly ToNativeType<T[number]>[]\n        : T extends readonly [...NativeType[]]\n          ? {\n              [K in keyof T]: ToNativeType<T[K]>;\n            }\n          : never;\n\n  /** Type conversion for foreign symbol return types and unsafe callback\n   * parameters.\n   *\n   * @category FFI\n   */\n  export type FromNativeType<T extends NativeType = NativeType> =\n    T extends NativeStructType\n      ? Uint8Array\n      : T extends NativeNumberType\n        ? T extends NativeU8Enum<infer U>\n          ? U\n          : T extends NativeI8Enum<infer U>\n            ? U\n            : T extends NativeU16Enum<infer U>\n              ? U\n              : T extends NativeI16Enum<infer U>\n                ? U\n                : T extends NativeU32Enum<infer U>\n                  ? U\n                  : T extends NativeI32Enum<infer U>\n                    ? U\n                    : number\n        : T extends NativeBigIntType\n          ? bigint\n          : T extends NativeBooleanType\n            ? boolean\n            : T extends NativePointerType\n              ? T extends NativeTypedPointer<infer U>\n                ? U | null\n                : PointerValue\n              : T extends NativeBufferType\n                ? PointerValue\n                : T extends NativeFunctionType\n                  ? T extends NativeTypedFunction<infer U>\n                    ? PointerObject<U> | null\n                    : PointerValue\n                  : never;\n\n  /** Type conversion for foreign symbol return types.\n   *\n   * @category FFI\n   */\n  export type FromNativeResultType<\n    T extends NativeResultType = NativeResultType,\n  > = T extends NativeStructType\n    ? Uint8Array\n    : T extends NativeNumberType\n      ? T extends NativeU8Enum<infer U>\n        ? U\n        : T extends NativeI8Enum<infer U>\n          ? U\n          : T extends NativeU16Enum<infer U>\n            ? U\n            : T extends NativeI16Enum<infer U>\n              ? U\n              : T extends NativeU32Enum<infer U>\n                ? U\n                : T extends NativeI32Enum<infer U>\n                  ? U\n                  : number\n      : T extends NativeBigIntType\n        ? bigint\n        : T extends NativeBooleanType\n          ? boolean\n          : T extends NativePointerType\n            ? T extends NativeTypedPointer<infer U>\n              ? U | null\n              : PointerValue\n            : T extends NativeBufferType\n              ? PointerValue\n              : T extends NativeFunctionType\n                ? T extends NativeTypedFunction<infer U>\n                  ? PointerObject<U> | null\n                  : PointerValue\n                : T extends NativeVoidType\n                  ? void\n                  : never;\n\n  /** @category FFI\n   */\n  export type FromNativeParameterTypes<T extends readonly NativeType[]> =\n    //\n    [T[number][]] extends [T]\n      ? FromNativeType<T[number]>[]\n      : [readonly T[number][]] extends [T]\n        ? readonly FromNativeType<T[number]>[]\n        : T extends readonly [...NativeType[]]\n          ? {\n              [K in keyof T]: FromNativeType<T[K]>;\n            }\n          : never;\n\n  /** The interface for a foreign function as defined by its parameter and result\n   * types.\n   *\n   * @category FFI\n   */\n  export interface ForeignFunction<\n    Parameters extends readonly NativeType[] = readonly NativeType[],\n    Result extends NativeResultType = NativeResultType,\n    NonBlocking extends boolean = boolean,\n  > {\n    /** Name of the symbol.\n     *\n     * Defaults to the key name in symbols object. */\n    name?: string;\n    /** The parameters of the foreign function. */\n    parameters: Parameters;\n    /** The result (return value) of the foreign function. */\n    result: Result;\n    /** When `true`, function calls will run on a dedicated blocking thread and\n     * will return a `Promise` resolving to the `result`. */\n    nonblocking?: NonBlocking;\n    /** When `true`, dlopen will not fail if the symbol is not found.\n     * Instead, the symbol will be set to `null`.\n     *\n     * @default {false} */\n    optional?: boolean;\n  }\n\n  /** @category FFI\n   */\n  export interface ForeignStatic<Type extends NativeType = NativeType> {\n    /** Name of the symbol, defaults to the key name in symbols object. */\n    name?: string;\n    /** The type of the foreign static value. */\n    type: Type;\n    /** When `true`, dlopen will not fail if the symbol is not found.\n     * Instead, the symbol will be set to `null`.\n     *\n     * @default {false} */\n    optional?: boolean;\n  }\n\n  /** A foreign library interface descriptor.\n   *\n   * @category FFI\n   */\n  export interface ForeignLibraryInterface {\n    [name: string]: ForeignFunction | ForeignStatic;\n  }\n\n  /** A utility type that infers a foreign symbol.\n   *\n   * @category FFI\n   */\n  export type StaticForeignSymbol<T extends ForeignFunction | ForeignStatic> =\n    T extends ForeignFunction\n      ? FromForeignFunction<T>\n      : T extends ForeignStatic\n        ? FromNativeType<T[\"type\"]>\n        : never;\n\n  /**  @category FFI\n   */\n  export type FromForeignFunction<T extends ForeignFunction> =\n    T[\"parameters\"] extends readonly []\n      ? () => StaticForeignSymbolReturnType<T>\n      : (\n          ...args: ToNativeParameterTypes<T[\"parameters\"]>\n        ) => StaticForeignSymbolReturnType<T>;\n\n  /** @category FFI\n   */\n  export type StaticForeignSymbolReturnType<T extends ForeignFunction> =\n    ConditionalAsync<T[\"nonblocking\"], FromNativeResultType<T[\"result\"]>>;\n\n  /** @category FFI\n   */\n  export type ConditionalAsync<\n    IsAsync extends boolean | undefined,\n    T,\n  > = IsAsync extends true ? Promise<T> : T;\n\n  /** A utility type that infers a foreign library interface.\n   *\n   * @category FFI\n   */\n  export type StaticForeignLibraryInterface<T extends ForeignLibraryInterface> =\n    {\n      [K in keyof T]: T[K][\"optional\"] extends true\n        ? StaticForeignSymbol<T[K]> | null\n        : StaticForeignSymbol<T[K]>;\n    };\n\n  /** A non-null pointer, represented as an object\n   * at runtime. The object's prototype is `null`\n   * and cannot be changed. The object cannot be\n   * assigned to either and is thus entirely read-only.\n   *\n   * To interact with memory through a pointer use the\n   * {@linkcode UnsafePointerView} class. To create a\n   * pointer from an address or the get the address of\n   * a pointer use the static methods of the\n   * {@linkcode UnsafePointer} class.\n   *\n   * @category FFI\n   */\n  export interface PointerObject<T = unknown> {\n    [brand]: T;\n  }\n\n  /** Pointers are represented either with a {@linkcode PointerObject}\n   * object or a `null` if the pointer is null.\n   *\n   * @category FFI\n   */\n  export type PointerValue<T = unknown> = null | PointerObject<T>;\n\n  /** A collection of static functions for interacting with pointer objects.\n   *\n   * @category FFI\n   */\n  export class UnsafePointer {\n    /** Create a pointer from a numeric value. This one is <i>really</i> dangerous! */\n    static create<T = unknown>(value: bigint): PointerValue<T>;\n    /** Returns `true` if the two pointers point to the same address. */\n    static equals<T = unknown>(a: PointerValue<T>, b: PointerValue<T>): boolean;\n    /** Return the direct memory pointer to the typed array in memory. */\n    static of<T = unknown>(\n      value: Deno.UnsafeCallback | BufferSource\n    ): PointerValue<T>;\n    /** Return a new pointer offset from the original by `offset` bytes. */\n    static offset<T = unknown>(\n      value: PointerObject,\n      offset: number\n    ): PointerValue<T>;\n    /** Get the numeric value of a pointer */\n    static value(value: PointerValue): bigint;\n  }\n\n  /** An unsafe pointer view to a memory location as specified by the `pointer`\n   * value. The `UnsafePointerView` API follows the standard built in interface\n   * {@linkcode DataView} for accessing the underlying types at an memory\n   * location (numbers, strings and raw bytes).\n   *\n   * @category FFI\n   */\n  export class UnsafePointerView {\n    constructor(pointer: PointerObject);\n\n    pointer: PointerObject;\n\n    /** Gets a boolean at the specified byte offset from the pointer. */\n    getBool(offset?: number): boolean;\n    /** Gets an unsigned 8-bit integer at the specified byte offset from the\n     * pointer. */\n    getUint8(offset?: number): number;\n    /** Gets a signed 8-bit integer at the specified byte offset from the\n     * pointer. */\n    getInt8(offset?: number): number;\n    /** Gets an unsigned 16-bit integer at the specified byte offset from the\n     * pointer. */\n    getUint16(offset?: number): number;\n    /** Gets a signed 16-bit integer at the specified byte offset from the\n     * pointer. */\n    getInt16(offset?: number): number;\n    /** Gets an unsigned 32-bit integer at the specified byte offset from the\n     * pointer. */\n    getUint32(offset?: number): number;\n    /** Gets a signed 32-bit integer at the specified byte offset from the\n     * pointer. */\n    getInt32(offset?: number): number;\n    /** Gets an unsigned 64-bit integer at the specified byte offset from the\n     * pointer. */\n    getBigUint64(offset?: number): bigint;\n    /** Gets a signed 64-bit integer at the specified byte offset from the\n     * pointer. */\n    getBigInt64(offset?: number): bigint;\n    /** Gets a signed 32-bit float at the specified byte offset from the\n     * pointer. */\n    getFloat32(offset?: number): number;\n    /** Gets a signed 64-bit float at the specified byte offset from the\n     * pointer. */\n    getFloat64(offset?: number): number;\n    /** Gets a pointer at the specified byte offset from the pointer */\n    getPointer<T = unknown>(offset?: number): PointerValue<T>;\n    /** Gets a C string (`null` terminated string) at the specified byte offset\n     * from the pointer. */\n    getCString(offset?: number): string;\n    /** Gets a C string (`null` terminated string) at the specified byte offset\n     * from the specified pointer. */\n    static getCString(pointer: PointerObject, offset?: number): string;\n    /** Gets an `ArrayBuffer` of length `byteLength` at the specified byte\n     * offset from the pointer. */\n    getArrayBuffer(byteLength: number, offset?: number): ArrayBuffer;\n    /** Gets an `ArrayBuffer` of length `byteLength` at the specified byte\n     * offset from the specified pointer. */\n    static getArrayBuffer(\n      pointer: PointerObject,\n      byteLength: number,\n      offset?: number\n    ): ArrayBuffer;\n    /** Copies the memory of the pointer into a typed array.\n     *\n     * Length is determined from the typed array's `byteLength`.\n     *\n     * Also takes optional byte offset from the pointer. */\n    copyInto(destination: BufferSource, offset?: number): void;\n    /** Copies the memory of the specified pointer into a typed array.\n     *\n     * Length is determined from the typed array's `byteLength`.\n     *\n     * Also takes optional byte offset from the pointer. */\n    static copyInto(\n      pointer: PointerObject,\n      destination: BufferSource,\n      offset?: number\n    ): void;\n  }\n\n  /** An unsafe pointer to a function, for calling functions that are not present\n   * as symbols.\n   *\n   * @category FFI\n   */\n  export class UnsafeFnPointer<const Fn extends ForeignFunction> {\n    /** The pointer to the function. */\n    pointer: PointerObject<Fn>;\n    /** The definition of the function. */\n    definition: Fn;\n\n    constructor(\n      pointer: PointerObject<NoInfer<Omit<Fn, \"nonblocking\">>>,\n      definition: Fn\n    );\n\n    /** Call the foreign function. */\n    call: FromForeignFunction<Fn>;\n  }\n\n  /** Definition of a unsafe callback function.\n   *\n   * @category FFI\n   */\n  export interface UnsafeCallbackDefinition<\n    Parameters extends readonly NativeType[] = readonly NativeType[],\n    Result extends NativeResultType = NativeResultType,\n  > {\n    /** The parameters of the callbacks. */\n    parameters: Parameters;\n    /** The current result of the callback. */\n    result: Result;\n  }\n\n  /** An unsafe callback function.\n   *\n   * @category FFI\n   */\n  export type UnsafeCallbackFunction<\n    Parameters extends readonly NativeType[] = readonly NativeType[],\n    Result extends NativeResultType = NativeResultType,\n  > = Parameters extends readonly []\n    ? () => ToNativeResultType<Result>\n    : (\n        ...args: FromNativeParameterTypes<Parameters>\n      ) => ToNativeResultType<Result>;\n\n  /** An unsafe function pointer for passing JavaScript functions as C function\n   * pointers to foreign function calls.\n   *\n   * The function pointer remains valid until the `close()` method is called.\n   *\n   * All `UnsafeCallback` are always thread safe in that they can be called from\n   * foreign threads without crashing. However, they do not wake up the Deno event\n   * loop by default.\n   *\n   * If a callback is to be called from foreign threads, use the `threadSafe()`\n   * static constructor or explicitly call `ref()` to have the callback wake up\n   * the Deno event loop when called from foreign threads. This also stops\n   * Deno's process from exiting while the callback still exists and is not\n   * unref'ed.\n   *\n   * Use `deref()` to then allow Deno's process to exit. Calling `deref()` on\n   * a ref'ed callback does not stop it from waking up the Deno event loop when\n   * called from foreign threads.\n   *\n   * @category FFI\n   */\n  export class UnsafeCallback<\n    const Definition extends\n      UnsafeCallbackDefinition = UnsafeCallbackDefinition,\n  > {\n    constructor(\n      definition: Definition,\n      callback: UnsafeCallbackFunction<\n        Definition[\"parameters\"],\n        Definition[\"result\"]\n      >\n    );\n\n    /** The pointer to the unsafe callback. */\n    readonly pointer: PointerObject<Definition>;\n    /** The definition of the unsafe callback. */\n    readonly definition: Definition;\n    /** The callback function. */\n    readonly callback: UnsafeCallbackFunction<\n      Definition[\"parameters\"],\n      Definition[\"result\"]\n    >;\n\n    /**\n     * Creates an {@linkcode UnsafeCallback} and calls `ref()` once to allow it to\n     * wake up the Deno event loop when called from foreign threads.\n     *\n     * This also stops Deno's process from exiting while the callback still\n     * exists and is not unref'ed.\n     */\n    static threadSafe<\n      Definition extends UnsafeCallbackDefinition = UnsafeCallbackDefinition,\n    >(\n      definition: Definition,\n      callback: UnsafeCallbackFunction<\n        Definition[\"parameters\"],\n        Definition[\"result\"]\n      >\n    ): UnsafeCallback<Definition>;\n\n    /**\n     * Increments the callback's reference counting and returns the new\n     * reference count.\n     *\n     * After `ref()` has been called, the callback always wakes up the\n     * Deno event loop when called from foreign threads.\n     *\n     * If the callback's reference count is non-zero, it keeps Deno's\n     * process from exiting.\n     */\n    ref(): number;\n\n    /**\n     * Decrements the callback's reference counting and returns the new\n     * reference count.\n     *\n     * Calling `unref()` does not stop a callback from waking up the Deno\n     * event loop when called from foreign threads.\n     *\n     * If the callback's reference counter is zero, it no longer keeps\n     * Deno's process from exiting.\n     */\n    unref(): number;\n\n    /**\n     * Removes the C function pointer associated with this instance.\n     *\n     * Continuing to use the instance or the C function pointer after closing\n     * the `UnsafeCallback` will lead to errors and crashes.\n     *\n     * Calling this method sets the callback's reference counting to zero,\n     * stops the callback from waking up the Deno event loop when called from\n     * foreign threads and no longer keeps Deno's process from exiting.\n     */\n    close(): void;\n  }\n\n  /** A dynamic library resource.  Use {@linkcode Deno.dlopen} to load a dynamic\n   * library and return this interface.\n   *\n   * @category FFI\n   */\n  export interface DynamicLibrary<S extends ForeignLibraryInterface> {\n    /** All of the registered library along with functions for calling them. */\n    symbols: StaticForeignLibraryInterface<S>;\n    /** Removes the pointers associated with the library symbols.\n     *\n     * Continuing to use symbols that are part of the library will lead to\n     * errors and crashes.\n     *\n     * Calling this method will also immediately set any references to zero and\n     * will no longer keep Deno's process from exiting.\n     */\n    close(): void;\n  }\n\n  /** Opens an external dynamic library and registers symbols, making foreign\n   * functions available to be called.\n   *\n   * Requires `allow-ffi` permission. Loading foreign dynamic libraries can in\n   * theory bypass all of the sandbox permissions. While it is a separate\n   * permission users should acknowledge in practice that is effectively the\n   * same as running with the `allow-all` permission.\n   *\n   * @example Given a C library which exports a foreign function named `add()`\n   *\n   * ```ts\n   * // Determine library extension based on\n   * // your OS.\n   * let libSuffix = \"\";\n   * switch (Deno.build.os) {\n   *   case \"windows\":\n   *     libSuffix = \"dll\";\n   *     break;\n   *   case \"darwin\":\n   *     libSuffix = \"dylib\";\n   *     break;\n   *   default:\n   *     libSuffix = \"so\";\n   *     break;\n   * }\n   *\n   * const libName = `./libadd.${libSuffix}`;\n   * // Open library and define exported symbols\n   * const dylib = Deno.dlopen(\n   *   libName,\n   *   {\n   *     \"add\": { parameters: [\"isize\", \"isize\"], result: \"isize\" },\n   *   } as const,\n   * );\n   *\n   * // Call the symbol `add`\n   * const result = dylib.symbols.add(35n, 34n); // 69n\n   *\n   * console.log(`Result from external addition of 35 and 34: ${result}`);\n   * ```\n   *\n   * @tags allow-ffi\n   * @category FFI\n   */\n  export function dlopen<const S extends ForeignLibraryInterface>(\n    filename: string | URL,\n    symbols: S\n  ): DynamicLibrary<S>;\n\n  /**\n   * A custom `HttpClient` for use with {@linkcode fetch} function. This is\n   * designed to allow custom certificates or proxies to be used with `fetch()`.\n   *\n   * @example ```ts\n   * const caCert = await Deno.readTextFile(\"./ca.pem\");\n   * const client = Deno.createHttpClient({ caCerts: [ caCert ] });\n   * const req = await fetch(\"https://myserver.com\", { client });\n   * ```\n   *\n   * @category Fetch\n   */\n  export class HttpClient implements Disposable {\n    /** Close the HTTP client. */\n    close(): void;\n\n    [Symbol.dispose](): void;\n  }\n\n  /**\n   * The options used when creating a {@linkcode Deno.HttpClient}.\n   *\n   * @category Fetch\n   */\n  export interface CreateHttpClientOptions {\n    /** A list of root certificates that will be used in addition to the\n     * default root certificates to verify the peer's certificate.\n     *\n     * Must be in PEM format. */\n    caCerts?: string[];\n    /** A HTTP proxy to use for new connections. */\n    proxy?: Proxy;\n    /** Sets the maximum number of idle connections per host allowed in the pool. */\n    poolMaxIdlePerHost?: number;\n    /** Set an optional timeout for idle sockets being kept-alive.\n     * Set to false to disable the timeout. */\n    poolIdleTimeout?: number | false;\n    /**\n     * Whether HTTP/1.1 is allowed or not.\n     *\n     * @default {true}\n     */\n    http1?: boolean;\n    /** Whether HTTP/2 is allowed or not.\n     *\n     * @default {true}\n     */\n    http2?: boolean;\n    /** Whether setting the host header is allowed or not.\n     *\n     * @default {false}\n     */\n    allowHost?: boolean;\n  }\n\n  /**\n   * The definition of a proxy when specifying\n   * {@linkcode Deno.CreateHttpClientOptions}.\n   *\n   * @category Fetch\n   */\n  export interface Proxy {\n    /** The string URL of the proxy server to use. */\n    url: string;\n    /** The basic auth credentials to be used against the proxy server. */\n    basicAuth?: BasicAuth;\n  }\n\n  /**\n   * Basic authentication credentials to be used with a {@linkcode Deno.Proxy}\n   * server when specifying {@linkcode Deno.CreateHttpClientOptions}.\n   *\n   * @category Fetch\n   */\n  export interface BasicAuth {\n    /** The username to be used against the proxy server. */\n    username: string;\n    /** The password to be used against the proxy server. */\n    password: string;\n  }\n\n  /** Create a custom HttpClient to use with {@linkcode fetch}. This is an\n   * extension of the web platform Fetch API which allows Deno to use custom\n   * TLS CA certificates and connect via a proxy while using `fetch()`.\n   *\n   * The `cert` and `key` options can be used to specify a client certificate\n   * and key to use when connecting to a server that requires client\n   * authentication (mutual TLS or mTLS). The `cert` and `key` options must be\n   * provided in PEM format.\n   *\n   * @example ```ts\n   * const caCert = await Deno.readTextFile(\"./ca.pem\");\n   * const client = Deno.createHttpClient({ caCerts: [ caCert ] });\n   * const response = await fetch(\"https://myserver.com\", { client });\n   * ```\n   *\n   * @example ```ts\n   * const client = Deno.createHttpClient({\n   *   proxy: { url: \"http://myproxy.com:8080\" }\n   * });\n   * const response = await fetch(\"https://myserver.com\", { client });\n   * ```\n   *\n   * @example ```ts\n   * const key = \"----BEGIN PRIVATE KEY----...\";\n   * const cert = \"----BEGIN CERTIFICATE----...\";\n   * const client = Deno.createHttpClient({ key, cert });\n   * const response = await fetch(\"https://myserver.com\", { client });\n   * ```\n   *\n   * @category Fetch\n   */\n  export function createHttpClient(\n    options:\n      | CreateHttpClientOptions\n      | (CreateHttpClientOptions & TlsCertifiedKeyPem)\n  ): HttpClient;\n\n  /** @category Network */\n  export interface NetAddr {\n    transport: \"tcp\" | \"udp\";\n    hostname: string;\n    port: number;\n  }\n\n  /** @category Network */\n  export interface UnixAddr {\n    transport: \"unix\" | \"unixpacket\";\n    path: string;\n  }\n\n  /** @category Network */\n  export type Addr = NetAddr | UnixAddr;\n\n  /** A generic network listener for stream-oriented protocols.\n   *\n   * @category Network\n   */\n  export interface Listener<T extends Conn = Conn, A extends Addr = Addr>\n    extends AsyncIterable<T>,\n      Disposable {\n    /** Waits for and resolves to the next connection to the `Listener`. */\n    accept(): Promise<T>;\n    /** Close closes the listener. Any pending accept promises will be rejected\n     * with errors. */\n    close(): void;\n    /** Return the address of the `Listener`. */\n    readonly addr: A;\n\n    [Symbol.asyncIterator](): AsyncIterableIterator<T>;\n\n    /**\n     * Make the listener block the event loop from finishing.\n     *\n     * Note: the listener blocks the event loop from finishing by default.\n     * This method is only meaningful after `.unref()` is called.\n     */\n    ref(): void;\n\n    /** Make the listener not block the event loop from finishing. */\n    unref(): void;\n  }\n\n  /** Specialized listener that accepts TLS connections.\n   *\n   * @category Network\n   */\n  export type TlsListener = Listener<TlsConn, NetAddr>;\n\n  /** Specialized listener that accepts TCP connections.\n   *\n   * @category Network\n   */\n  export type TcpListener = Listener<TcpConn, NetAddr>;\n\n  /** Specialized listener that accepts Unix connections.\n   *\n   * @category Network\n   */\n  export type UnixListener = Listener<UnixConn, UnixAddr>;\n\n  /** @category Network */\n  export interface Conn<A extends Addr = Addr> extends Disposable {\n    /** Read the incoming data from the connection into an array buffer (`p`).\n     *\n     * Resolves to either the number of bytes read during the operation or EOF\n     * (`null`) if there was nothing more to read.\n     *\n     * It is possible for a read to successfully return with `0` bytes. This\n     * does not indicate EOF.\n     *\n     * **It is not guaranteed that the full buffer will be read in a single\n     * call.**\n     *\n     * ```ts\n     * // If the text \"hello world\" is received by the client:\n     * const conn = await Deno.connect({ hostname: \"example.com\", port: 80 });\n     * const buf = new Uint8Array(100);\n     * const numberOfBytesRead = await conn.read(buf); // 11 bytes\n     * const text = new TextDecoder().decode(buf);  // \"hello world\"\n     * ```\n     *\n     * @category I/O\n     */\n    read(p: Uint8Array): Promise<number | null>;\n    /** Write the contents of the array buffer (`p`) to the connection.\n     *\n     * Resolves to the number of bytes written.\n     *\n     * **It is not guaranteed that the full buffer will be written in a single\n     * call.**\n     *\n     * ```ts\n     * const conn = await Deno.connect({ hostname: \"example.com\", port: 80 });\n     * const encoder = new TextEncoder();\n     * const data = encoder.encode(\"Hello world\");\n     * const bytesWritten = await conn.write(data); // 11\n     * ```\n     *\n     * @category I/O\n     */\n    write(p: Uint8Array): Promise<number>;\n    /** Closes the connection, freeing the resource.\n     *\n     * ```ts\n     * const conn = await Deno.connect({ hostname: \"example.com\", port: 80 });\n     *\n     * // ...\n     *\n     * conn.close();\n     * ```\n     */\n    close(): void;\n    /** The local address of the connection. */\n    readonly localAddr: A;\n    /** The remote address of the connection. */\n    readonly remoteAddr: A;\n    /** Shuts down (`shutdown(2)`) the write side of the connection. Most\n     * callers should just use `close()`. */\n    closeWrite(): Promise<void>;\n\n    /** Make the connection block the event loop from finishing.\n     *\n     * Note: the connection blocks the event loop from finishing by default.\n     * This method is only meaningful after `.unref()` is called.\n     */\n    ref(): void;\n    /** Make the connection not block the event loop from finishing. */\n    unref(): void;\n\n    readonly readable: ReadableStream<Uint8Array>;\n    readonly writable: WritableStream<Uint8Array>;\n  }\n\n  /** @category Network */\n  export interface TlsHandshakeInfo {\n    /**\n     * Contains the ALPN protocol selected during negotiation with the server.\n     * If no ALPN protocol selected, returns `null`.\n     */\n    alpnProtocol: string | null;\n  }\n\n  /** @category Network */\n  export interface TlsConn extends Conn<NetAddr> {\n    /** Runs the client or server handshake protocol to completion if that has\n     * not happened yet. Calling this method is optional; the TLS handshake\n     * will be completed automatically as soon as data is sent or received. */\n    handshake(): Promise<TlsHandshakeInfo>;\n  }\n\n  /** @category Network */\n  export interface ListenOptions {\n    /** The port to listen on.\n     *\n     * Set to `0` to listen on any available port.\n     */\n    port: number;\n    /** A literal IP address or host name that can be resolved to an IP address.\n     *\n     * __Note about `0.0.0.0`__ While listening `0.0.0.0` works on all platforms,\n     * the browsers on Windows don't work with the address `0.0.0.0`.\n     * You should show the message like `server running on localhost:8080` instead of\n     * `server running on 0.0.0.0:8080` if your program supports Windows.\n     *\n     * @default {\"0.0.0.0\"} */\n    hostname?: string;\n  }\n\n  /** @category Network */\n  export interface TcpListenOptions extends ListenOptions {}\n\n  /** Listen announces on the local transport address.\n   *\n   * ```ts\n   * const listener1 = Deno.listen({ port: 80 })\n   * const listener2 = Deno.listen({ hostname: \"192.0.2.1\", port: 80 })\n   * const listener3 = Deno.listen({ hostname: \"[2001:db8::1]\", port: 80 });\n   * const listener4 = Deno.listen({ hostname: \"golang.org\", port: 80, transport: \"tcp\" });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function listen(\n    options: TcpListenOptions & { transport?: \"tcp\" }\n  ): TcpListener;\n\n  /** Options which can be set when opening a Unix listener via\n   * {@linkcode Deno.listen} or {@linkcode Deno.listenDatagram}.\n   *\n   * @category Network\n   */\n  export interface UnixListenOptions {\n    /** A path to the Unix Socket. */\n    path: string;\n  }\n\n  /** Listen announces on the local transport address.\n   *\n   * ```ts\n   * const listener = Deno.listen({ path: \"/foo/bar.sock\", transport: \"unix\" })\n   * ```\n   *\n   * Requires `allow-read` and `allow-write` permission.\n   *\n   * @tags allow-read, allow-write\n   * @category Network\n   */\n  // deno-lint-ignore adjacent-overload-signatures\n  export function listen(\n    options: UnixListenOptions & { transport: \"unix\" }\n  ): UnixListener;\n\n  /**\n   * Provides certified key material from strings. The key material is provided in\n   * `PEM`-format (Privacy Enhanced Mail, https://www.rfc-editor.org/rfc/rfc1422) which can be identified by having\n   * `-----BEGIN-----` and `-----END-----` markers at the beginning and end of the strings. This type of key is not compatible\n   * with `DER`-format keys which are binary.\n   *\n   * Deno supports RSA, EC, and PKCS8-format keys.\n   *\n   * ```ts\n   * const key = {\n   *  key: \"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\",\n   *  cert: \"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n\" }\n   * };\n   * ```\n   *\n   * @category Network\n   */\n  export interface TlsCertifiedKeyPem {\n    /** The format of this key material, which must be PEM. */\n    keyFormat?: \"pem\";\n    /** Private key in `PEM` format. RSA, EC, and PKCS8-format keys are supported. */\n    key: string;\n    /** Certificate chain in `PEM` format. */\n    cert: string;\n  }\n\n  /** @category Network */\n  export interface ListenTlsOptions extends TcpListenOptions {\n    transport?: \"tcp\";\n\n    /** Application-Layer Protocol Negotiation (ALPN) protocols to announce to\n     * the client. If not specified, no ALPN extension will be included in the\n     * TLS handshake.\n     */\n    alpnProtocols?: string[];\n  }\n\n  /** Listen announces on the local transport address over TLS (transport layer\n   * security).\n   *\n   * ```ts\n   * using listener = Deno.listenTls({\n   *   port: 443,\n   *   cert: Deno.readTextFileSync(\"./server.crt\"),\n   *   key: Deno.readTextFileSync(\"./server.key\"),\n   * });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function listenTls(\n    options: ListenTlsOptions & TlsCertifiedKeyPem\n  ): TlsListener;\n\n  /** @category Network */\n  export interface ConnectOptions {\n    /** The port to connect to. */\n    port: number;\n    /** A literal IP address or host name that can be resolved to an IP address.\n     * If not specified,\n     *\n     * @default {\"127.0.0.1\"} */\n    hostname?: string;\n    transport?: \"tcp\";\n  }\n\n  /**\n   * Connects to the hostname (default is \"127.0.0.1\") and port on the named\n   * transport (default is \"tcp\"), and resolves to the connection (`Conn`).\n   *\n   * ```ts\n   * const conn1 = await Deno.connect({ port: 80 });\n   * const conn2 = await Deno.connect({ hostname: \"192.0.2.1\", port: 80 });\n   * const conn3 = await Deno.connect({ hostname: \"[2001:db8::1]\", port: 80 });\n   * const conn4 = await Deno.connect({ hostname: \"golang.org\", port: 80, transport: \"tcp\" });\n   * ```\n   *\n   * Requires `allow-net` permission for \"tcp\".\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function connect(options: ConnectOptions): Promise<TcpConn>;\n\n  /** @category Network */\n  export interface TcpConn extends Conn<NetAddr> {\n    /**\n     * Enable/disable the use of Nagle's algorithm.\n     *\n     * @param [noDelay=true]\n     */\n    setNoDelay(noDelay?: boolean): void;\n    /** Enable/disable keep-alive functionality. */\n    setKeepAlive(keepAlive?: boolean): void;\n  }\n\n  /** @category Network */\n  export interface UnixConnectOptions {\n    transport: \"unix\";\n    path: string;\n  }\n\n  /** @category Network */\n  export interface UnixConn extends Conn<UnixAddr> {}\n\n  /** Connects to the hostname (default is \"127.0.0.1\") and port on the named\n   * transport (default is \"tcp\"), and resolves to the connection (`Conn`).\n   *\n   * ```ts\n   * const conn1 = await Deno.connect({ port: 80 });\n   * const conn2 = await Deno.connect({ hostname: \"192.0.2.1\", port: 80 });\n   * const conn3 = await Deno.connect({ hostname: \"[2001:db8::1]\", port: 80 });\n   * const conn4 = await Deno.connect({ hostname: \"golang.org\", port: 80, transport: \"tcp\" });\n   * const conn5 = await Deno.connect({ path: \"/foo/bar.sock\", transport: \"unix\" });\n   * ```\n   *\n   * Requires `allow-net` permission for \"tcp\" and `allow-read` for \"unix\".\n   *\n   * @tags allow-net, allow-read\n   * @category Network\n   */\n  // deno-lint-ignore adjacent-overload-signatures\n  export function connect(options: UnixConnectOptions): Promise<UnixConn>;\n\n  /** @category Network */\n  export interface ConnectTlsOptions {\n    /** The port to connect to. */\n    port: number;\n    /** A literal IP address or host name that can be resolved to an IP address.\n     *\n     * @default {\"127.0.0.1\"} */\n    hostname?: string;\n    /** A list of root certificates that will be used in addition to the\n     * default root certificates to verify the peer's certificate.\n     *\n     * Must be in PEM format. */\n    caCerts?: string[];\n    /** Application-Layer Protocol Negotiation (ALPN) protocols supported by\n     * the client. If not specified, no ALPN extension will be included in the\n     * TLS handshake.\n     */\n    alpnProtocols?: string[];\n  }\n\n  /** Establishes a secure connection over TLS (transport layer security) using\n   * an optional list of CA certs, hostname (default is \"127.0.0.1\") and port.\n   *\n   * The CA cert list is optional and if not included Mozilla's root\n   * certificates will be used (see also https://github.com/ctz/webpki-roots for\n   * specifics).\n   *\n   * Mutual TLS (mTLS or client certificates) are supported by providing a\n   * `key` and `cert` in the options as PEM-encoded strings.\n   *\n   * ```ts\n   * const caCert = await Deno.readTextFile(\"./certs/my_custom_root_CA.pem\");\n   * const conn1 = await Deno.connectTls({ port: 80 });\n   * const conn2 = await Deno.connectTls({ caCerts: [caCert], hostname: \"192.0.2.1\", port: 80 });\n   * const conn3 = await Deno.connectTls({ hostname: \"[2001:db8::1]\", port: 80 });\n   * const conn4 = await Deno.connectTls({ caCerts: [caCert], hostname: \"golang.org\", port: 80});\n   *\n   * const key = \"----BEGIN PRIVATE KEY----...\";\n   * const cert = \"----BEGIN CERTIFICATE----...\";\n   * const conn5 = await Deno.connectTls({ port: 80, key, cert });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function connectTls(\n    options: ConnectTlsOptions | (ConnectTlsOptions & TlsCertifiedKeyPem)\n  ): Promise<TlsConn>;\n\n  /** @category Network */\n  export interface StartTlsOptions {\n    /** A literal IP address or host name that can be resolved to an IP address.\n     *\n     * @default {\"127.0.0.1\"} */\n    hostname?: string;\n    /** A list of root certificates that will be used in addition to the\n     * default root certificates to verify the peer's certificate.\n     *\n     * Must be in PEM format. */\n    caCerts?: string[];\n    /** Application-Layer Protocol Negotiation (ALPN) protocols to announce to\n     * the client. If not specified, no ALPN extension will be included in the\n     * TLS handshake.\n     */\n    alpnProtocols?: string[];\n  }\n\n  /** Start TLS handshake from an existing connection using an optional list of\n   * CA certificates, and hostname (default is \"127.0.0.1\"). Specifying CA certs\n   * is optional. By default the configured root certificates are used. Using\n   * this function requires that the other end of the connection is prepared for\n   * a TLS handshake.\n   *\n   * Note that this function *consumes* the TCP connection passed to it, thus the\n   * original TCP connection will be unusable after calling this. Additionally,\n   * you need to ensure that the TCP connection is not being used elsewhere when\n   * calling this function in order for the TCP connection to be consumed properly.\n   * For instance, if there is a `Promise` that is waiting for read operation on\n   * the TCP connection to complete, it is considered that the TCP connection is\n   * being used elsewhere. In such a case, this function will fail.\n   *\n   * ```ts\n   * const conn = await Deno.connect({ port: 80, hostname: \"127.0.0.1\" });\n   * const caCert = await Deno.readTextFile(\"./certs/my_custom_root_CA.pem\");\n   * // `conn` becomes unusable after calling `Deno.startTls`\n   * const tlsConn = await Deno.startTls(conn, { caCerts: [caCert], hostname: \"localhost\" });\n   * ```\n   *\n   * Requires `allow-net` permission.\n   *\n   * @tags allow-net\n   * @category Network\n   */\n  export function startTls(\n    conn: TcpConn,\n    options?: StartTlsOptions\n  ): Promise<TlsConn>;\n\n  export {}; // only export exports\n}\n"
  },
  {
    "path": "frontend/public/index.d.ts",
    "content": "import { KomodoClient as Client, Types as KomodoTypes } from \"./client/lib.js\";\nimport \"./deno.d.ts\";\n\ndeclare global {\n  // =================\n  // 🔴 Docker Compose\n  // =================\n\n  /**\n   * Docker Compose configuration interface\n   */\n  export interface DockerCompose {\n    /** Version of the Compose file format */\n    version?: string;\n    /** Defines services within the Docker Compose file */\n    services: Record<string, DockerComposeService>;\n    /** Defines volumes in the Docker Compose file */\n    volumes?: Record<string, DockerComposeVolume>;\n    /** Defines networks in the Docker Compose file */\n    networks?: Record<string, DockerComposeNetwork>;\n  }\n\n  /**\n   * Describes a service within Docker Compose\n   */\n  export interface DockerComposeService {\n    /** Docker image to use */\n    image?: string;\n    /** Build configuration for the service */\n    build?: DockerComposeServiceBuild;\n    /** Ports to map, supporting single strings or mappings */\n    ports?: (string | DockerComposeServicePortMapping)[];\n    /** Environment variables to set within the container */\n    environment?: Record<string, string>;\n    /** Volumes to mount */\n    volumes?: (string | DockerComposeServiceVolumeMount)[];\n    /** Networks to attach the service to */\n    networks?: string[];\n    /** Dependencies of the service */\n    depends_on?: string[];\n    /** Command to override the default CMD */\n    command?: string | string[];\n    /** Entrypoint to override the default ENTRYPOINT */\n    entrypoint?: string | string[];\n    /** Container name */\n    container_name?: string;\n    /** Healthcheck configuration for the service */\n    healthcheck?: DockerComposeServiceHealthcheck;\n    /** Logging options for the service */\n    logging?: DockerComposeServiceLogging;\n    /** Deployment settings for the service */\n    deploy?: DockerComposeServiceDeploy;\n    /** Restart policy */\n    restart?: string;\n    /** Security options */\n    security_opt?: string[];\n    /** Ulimits configuration */\n    ulimits?: Record<string, DockerComposeServiceUlimit>;\n    /** Secrets to be used by the service */\n    secrets?: string[];\n    /** Configuration items */\n    configs?: string[];\n    /** Labels to apply to the service */\n    labels?: Record<string, string>;\n    /** Number of CPU units assigned */\n    cpus?: string | number;\n    /** Memory limit */\n    mem_limit?: string;\n    /** CPU shares for container allocation */\n    cpu_shares?: number;\n    /** Extra hosts for the service */\n    extra_hosts?: string[];\n    [key: string]: unknown;\n  }\n\n  /**\n   * Configuration for Docker build\n   */\n  export interface DockerComposeServiceBuild {\n    /** Build context path */\n    context: string;\n    /** Dockerfile path within the context */\n    dockerfile?: string;\n    /** Build arguments to pass */\n    args?: Record<string, string>;\n    /** Sources for cache imports */\n    cache_from?: string[];\n    /** Labels for the build */\n    labels?: Record<string, string>;\n    /** Network mode for build process */\n    network?: string;\n    /** Target build stage */\n    target?: string;\n    /** Shared memory size */\n    shm_size?: string;\n    /** Secrets for the build process */\n    secrets?: string[];\n    /** Extra hosts for build process */\n    extra_hosts?: string[];\n  }\n\n  /**\n   * Port mapping configuration\n   */\n  export interface DockerComposeServicePortMapping {\n    /** Target port inside the container */\n    target: number;\n    /** Published port on the host */\n    published?: number;\n    /** Protocol used for the port (tcp/udp) */\n    protocol?: \"tcp\" | \"udp\";\n    /** Mode for port publishing */\n    mode?: \"host\" | \"ingress\";\n  }\n\n  /**\n   * Volume mount configuration\n   */\n  export interface DockerComposeServiceVolumeMount {\n    /** Type of volume mount */\n    type: \"volume\" | \"bind\" | \"tmpfs\";\n    /** Source path or name */\n    source: string;\n    /** Target path within the container */\n    target: string;\n    /** Whether the volume is read-only */\n    read_only?: boolean;\n  }\n\n  /**\n   * Healthcheck configuration for a service\n   */\n  export interface DockerComposeServiceHealthcheck {\n    /** Command to check health */\n    test: string | string[];\n    /** Interval between checks */\n    interval?: string;\n    /** Timeout for each check */\n    timeout?: string;\n    /** Maximum number of retries */\n    retries?: number;\n    /** Initial delay before checks start */\n    start_period?: string;\n  }\n\n  /**\n   * Logging configuration for a service\n   */\n  export interface DockerComposeServiceLogging {\n    /** Logging driver */\n    driver: string;\n    /** Options for the logging driver */\n    options?: Record<string, string>;\n  }\n\n  /**\n   * Deployment configuration for a service\n   */\n  export interface DockerComposeServiceDeploy {\n    /** Number of replicas */\n    replicas?: number;\n    /** Update configuration */\n    update_config?: DockerComposeServiceDeploy;\n    /** Restart policy */\n    restart_policy?: DockerComposeServiceDeployRestartPolicy;\n  }\n\n  /**\n   * Update configuration during deployment\n   */\n  export interface DockerComposeServiceDeployUpdateConfig {\n    /** Number of containers updated in parallel */\n    parallelism?: number;\n    /** Delay between updates */\n    delay?: string;\n    /** Action on failure */\n    failure_action?: string;\n    /** Order of updates */\n    order?: string;\n  }\n\n  /**\n   * Restart policy configuration\n   */\n  export interface DockerComposeServiceDeployRestartPolicy {\n    /** Condition for restart */\n    condition: \"none\" | \"on-failure\" | \"any\";\n    /** Delay before restarting */\n    delay?: string;\n    /** Maximum number of restart attempts */\n    max_attempts?: number;\n    /** Time window for restart attempts */\n    window?: string;\n  }\n\n  /**\n   * Ulimit configuration\n   */\n  export interface DockerComposeServiceUlimit {\n    /** Soft limit */\n    soft: number;\n    /** Hard limit */\n    hard: number;\n  }\n\n  /**\n   * Volume configuration in Docker Compose\n   */\n  export interface DockerComposeVolume {\n    /** Volume driver to use */\n    driver?: string;\n    /** Driver options */\n    driver_opts?: Record<string, string>;\n    /** External volume identifier */\n    external?: boolean | string;\n  }\n\n  /**\n   * Network configuration in Docker Compose\n   */\n  export interface DockerComposeNetwork {\n    /** Network driver */\n    driver?: string;\n    /** Indicates if network is external */\n    external?: boolean;\n  }\n\n  // =====================\n  // 🔴 YAML De/serializer\n  // =====================\n\n  // https://jsr.io/@std/yaml\n\n  export type YamlSchemaType =\n    | \"failsafe\"\n    | \"json\"\n    | \"core\"\n    | \"default\"\n    | \"extended\";\n\n  export type YamlStyleVariant =\n    | \"lowercase\"\n    | \"uppercase\"\n    | \"camelcase\"\n    | \"decimal\"\n    | \"binary\"\n    | \"octal\"\n    | \"hexadecimal\";\n\n  /** Options for `YAML.stringify` */\n  export type YamlStringifyOptions = {\n    /**\n     * Indentation width to use (in spaces).\n     *\n     * @default {2}\n     */\n    indent?: number;\n    /**\n     * When true, adds an indentation level to array elements.\n     *\n     * @default {true}\n     */\n    arrayIndent?: boolean;\n    /**\n     * Do not throw on invalid types (like function in the safe schema) and skip\n     * pairs and single values with such types.\n     *\n     * @default {false}\n     */\n    skipInvalid?: boolean;\n    /**\n     * Specifies level of nesting, when to switch from block to flow style for\n     * collections. `-1` means block style everywhere.\n     *\n     * @default {-1}\n     */\n    flowLevel?: number;\n    /** Each tag may have own set of styles.\t- \"tag\" => \"style\" map. */\n    styles?: Record<string, YamlStyleVariant>;\n    /**\n     * Name of the schema to use.\n     *\n     * @default {\"default\"}\n     */\n    schema?: YamlSchemaType;\n    /**\n     * If true, sort keys when dumping YAML in ascending, ASCII character order.\n     * If a function, use the function to sort the keys.\n     * If a function is specified, the function must return a negative value\n     * if first argument is less than second argument, zero if they're equal\n     * and a positive value otherwise.\n     *\n     * @default {false}\n     */\n    sortKeys?: boolean | ((a: string, b: string) => number);\n    /**\n     * Set max line width.\n     *\n     * @default {80}\n     */\n    lineWidth?: number;\n    /**\n     * If false, don't convert duplicate objects into references.\n     *\n     * @default {true}\n     */\n    useAnchors?: boolean;\n    /**\n     * If false don't try to be compatible with older yaml versions.\n     * Currently: don't quote \"yes\", \"no\" and so on,\n     * as required for YAML 1.1.\n     *\n     * @default {true}\n     */\n    compatMode?: boolean;\n    /**\n     * If true flow sequences will be condensed, omitting the\n     * space between `key: value` or `a, b`. Eg. `'[a,b]'` or `{a:{b:c}}`.\n     * Can be useful when using yaml for pretty URL query params\n     * as spaces are %-encoded.\n     *\n     * @default {false}\n     */\n    condenseFlow?: boolean;\n  };\n\n  /** Options for `YAML.parse` */\n  export interface YamlParseOptions {\n    /**\n     * Name of the schema to use.\n     *\n     * @default {\"default\"}\n     */\n    schema?: YamlSchemaType;\n    /**\n     * If `true`, duplicate keys will overwrite previous values. Otherwise,\n     * duplicate keys will throw a {@linkcode SyntaxError}.\n     *\n     * @default {false}\n     */\n    allowDuplicateKeys?: boolean;\n    /**\n     * If defined, a function to call on warning messages taking an\n     * {@linkcode Error} as its only argument.\n     */\n    onWarning?(error: Error): void;\n  }\n\n  // ===============\n  // 🔴 Cargo TOML 🦀\n  // ===============\n\n  /**\n   * Represents the structure of a Cargo.toml manifest file.\n   */\n  export interface CargoToml {\n    /**\n     * Information about the main package in the Cargo project.\n     */\n    package?: CargoTomlPackage;\n\n    /**\n     * Dependencies required by the project, organized into normal, development, and build dependencies.\n     */\n    dependencies?: CargoTomlDependencies;\n\n    /**\n     * Development dependencies required by the project.\n     */\n    devDependencies?: CargoTomlDependencies;\n\n    /**\n     * Build dependencies required by the project.\n     */\n    buildDependencies?: CargoTomlDependencies;\n\n    /**\n     * Features available in the package, each as an array of dependency names or other features.\n     */\n    features?: Record<string, string[]>;\n\n    /**\n     * Build profiles available in the package, allowing for profile-specific configurations.\n     */\n    profile?: CargoTomlProfiles;\n\n    /**\n     * Path to the custom build script for the package, if applicable.\n     */\n    build?: string;\n\n    /**\n     * Workspace configuration for multi-package Cargo projects.\n     */\n    workspace?: CargoTomlWorkspace;\n\n    /**\n     * Additional metadata ignored by Cargo but potentially used by other tools.\n     */\n    [key: string]: any;\n  }\n\n  /**\n   * Metadata for the main package in the Cargo project.\n   */\n  export interface CargoTomlPackage {\n    /**\n     * The name of the package, used by Cargo and for crate publishing.\n     */\n    name: string;\n\n    /**\n     * The version of the package, following Semantic Versioning.\n     */\n    version: string;\n\n    /**\n     * List of author names or emails.\n     */\n    authors?: string[];\n\n    /**\n     * The Rust edition for this package.\n     */\n    edition?: \"2015\" | \"2018\" | \"2021\";\n\n    /**\n     * Short description of the package.\n     */\n    description?: string;\n\n    /**\n     * The license for the package, specified as a SPDX identifier.\n     */\n    license?: string;\n\n    /**\n     * Path to a custom license file for the package.\n     */\n    licenseFile?: string;\n\n    /**\n     * URL to the package documentation.\n     */\n    documentation?: string;\n\n    /**\n     * URL to the package homepage.\n     */\n    homepage?: string;\n\n    /**\n     * URL to the package repository.\n     */\n    repository?: string;\n\n    /**\n     * Path to the README file for the package.\n     */\n    readme?: string;\n\n    /**\n     * List of keywords for the package, used for search optimization.\n     */\n    keywords?: string[];\n\n    /**\n     * List of categories that the package belongs to.\n     */\n    categories?: string[];\n\n    /**\n     * Workspace that this package belongs to, if any.\n     */\n    workspace?: string;\n\n    /**\n     * Path to a build script for the package.\n     */\n    build?: string;\n\n    /**\n     * Name of a native library to link with, if applicable.\n     */\n    links?: string;\n\n    /**\n     * List of paths to exclude from the package.\n     */\n    exclude?: string[];\n\n    /**\n     * List of paths to include in the package.\n     */\n    include?: string[];\n\n    /**\n     * Indicates whether the package should be published to crates.io.\n     */\n    publish?: boolean;\n\n    /**\n     * Arbitrary metadata that is ignored by Cargo but can be used by other tools.\n     */\n    metadata?: Record<string, any>;\n\n    /**\n     * Auto-enable binaries for the package.\n     */\n    autobins?: boolean;\n\n    /**\n     * Auto-enable examples for the package.\n     */\n    autoexamples?: boolean;\n\n    /**\n     * Auto-enable tests for the package.\n     */\n    autotests?: boolean;\n\n    /**\n     * Auto-enable benchmarks for the package.\n     */\n    autobenches?: boolean;\n\n    /**\n     * Specifies the version of dependency resolution to use.\n     */\n    resolver?: \"1\" | \"2\";\n  }\n\n  /**\n   * A map of dependencies in the Cargo manifest, with each dependency represented by its name.\n   */\n  export type CargoTomlDependencies = Record<string, CargoTomlDependency>;\n\n  /**\n   * Information about a specific dependency in the Cargo manifest.\n   */\n  export type CargoTomlDependency =\n    | string\n    | {\n        /**\n         * Version requirement for the dependency.\n         */\n        version?: string;\n\n        /**\n         * Path to a local dependency.\n         */\n        path?: string;\n\n        /**\n         * Name of the registry to use for this dependency.\n         */\n        registry?: string;\n\n        /**\n         * URL to a Git repository for this dependency.\n         */\n        git?: string;\n\n        /**\n         * Branch to use for a Git dependency.\n         */\n        branch?: string;\n\n        /**\n         * Tag to use for a Git dependency.\n         */\n        tag?: string;\n\n        /**\n         * Specific revision to use for a Git dependency.\n         */\n        rev?: string;\n\n        /**\n         * Marks this dependency as optional.\n         */\n        optional?: boolean;\n\n        /**\n         * Enables default features for this dependency.\n         */\n        defaultFeatures?: boolean;\n\n        /**\n         * List of features to enable for this dependency.\n         */\n        features?: string[];\n\n        /**\n         * Renames the dependency package name.\n         */\n        package?: string;\n      };\n\n  /**\n   * Defines available profiles for building the package.\n   */\n  export interface CargoTomlProfiles {\n    /**\n     * Development profile configuration.\n     */\n    dev?: CargoTomlProfile;\n\n    /**\n     * Release profile configuration.\n     */\n    release?: CargoTomlProfile;\n\n    /**\n     * Test profile configuration.\n     */\n    test?: CargoTomlProfile;\n\n    /**\n     * Benchmark profile configuration.\n     */\n    bench?: CargoTomlProfile;\n\n    /**\n     * Documentation profile configuration.\n     */\n    doc?: CargoTomlProfile;\n\n    /**\n     * Additional custom profiles.\n     */\n    [profileName: string]: CargoTomlProfile | undefined;\n  }\n\n  /**\n   * Configuration for an individual build profile.\n   */\n  export interface CargoTomlProfile {\n    /**\n     * Profile that this profile inherits from.\n     */\n    inherits?: string;\n\n    /**\n     * Optimization level for the profile.\n     */\n    optLevel?: \"0\" | \"1\" | \"2\" | \"3\" | \"s\" | \"z\";\n\n    /**\n     * Enables debug information, either as a boolean or a level.\n     */\n    debug?: boolean | number;\n\n    /**\n     * Controls how debug information is split.\n     */\n    splitDebugInfo?: \"unpacked\" | \"packed\" | \"off\";\n\n    /**\n     * Enables or disables debug assertions.\n     */\n    debugAssertions?: boolean;\n\n    /**\n     * Enables or disables overflow checks.\n     */\n    overflowChecks?: boolean;\n\n    /**\n     * Enables or disables unit testing for the profile.\n     */\n    test?: boolean;\n\n    /**\n     * Link-time optimization settings for the profile.\n     */\n    lto?: boolean | \"thin\" | \"fat\";\n\n    /**\n     * Panic strategy for the profile.\n     */\n    panic?: \"unwind\" | \"abort\";\n\n    /**\n     * Enables or disables incremental compilation.\n     */\n    incremental?: boolean;\n\n    /**\n     * Number of code generation units for parallelism.\n     */\n    codegenUnits?: number;\n\n    /**\n     * Enables or disables the use of runtime paths.\n     */\n    rpath?: boolean;\n\n    /**\n     * Specifies stripping options for the binary.\n     */\n    strip?: boolean | \"debuginfo\" | \"symbols\";\n\n    /**\n     * Additional custom profile fields.\n     */\n    [key: string]: any;\n  }\n\n  /**\n   * Defines workspace-specific settings for a Cargo project.\n   */\n  export interface CargoTomlWorkspace {\n    /**\n     * Members of the workspace.\n     */\n    members?: string[];\n\n    /**\n     * Paths to exclude from the workspace.\n     */\n    exclude?: string[];\n\n    /**\n     * Members to include by default when building the workspace.\n     */\n    defaultMembers?: string[];\n\n    /**\n     * Common Information about the packages in the Cargo workspace.\n     */\n    package?: CargoTomlPackage;\n\n    /**\n     * Additional custom workspace fields.\n     */\n    [key: string]: any;\n  }\n\n  // =====================\n  // 🔴 TOML De/serializer\n  // =====================\n\n  // https://jsr.io/@std/toml\n\n  export interface TomlStringifyOptions {\n    /**\n     * Define if the keys should be aligned or not.\n     *\n     * @default {false}\n     */\n    keyAlignment?: boolean;\n  }\n\n  /** Pre initialized Komodo client */\n  var komodo: ReturnType<typeof Client>;\n  /** KomodoClient initializer */\n  var KomodoClient: typeof Client;\n  /** All Komodo Types */\n  export import Types = KomodoTypes;\n  /** The incoming arguments */\n  var ARGS: {\n    WEBHOOK_BRANCH?: string;\n    WEBHOOK_BODY?: any;\n  } & Record<string, any>;\n  /** YAML parsing utilities */\n  var YAML: {\n    /**\n     * Converts a JavaScript object or value to a YAML document string.\n     *\n     * @example Usage\n     * ```ts\n     * const data = { id: 1, name: \"Alice\" };\n     *\n     * const yaml = YAML.stringify(data);\n     *\n     * assertEquals(yaml, \"id: 1\\nname: Alice\\n\");\n     * ```\n     *\n     * @throws {TypeError} If `data` contains invalid types.\n     * @param data The data to serialize.\n     * @param options The options for serialization.\n     * @returns A YAML string.\n     */\n    stringify: (data: unknown, options?: YamlStringifyOptions) => string;\n    /**\n     * Parse and return a YAML string as a parsed YAML document object.\n     *\n     * Note: This does not support functions. Untrusted data is safe to parse.\n     *\n     * @example Usage\n     * ```ts\n     * const data = YAML.parse(`\n     * id: 1\n     * name: Alice\n     * `);\n     *\n     * assertEquals(data, { id: 1, name: \"Alice\" });\n     * ```\n     *\n     * @throws {SyntaxError} Throws error on invalid YAML.\n     * @param content YAML string to parse.\n     * @param options Parsing options.\n     * @returns Parsed document.\n     */\n    parse: (content: string, options?: YamlParseOptions) => unknown;\n    /**\n     * Same as `YAML.parse`, but understands multi-document YAML sources, and\n     * returns multiple parsed YAML document objects.\n     *\n     * @example Usage\n     * ```ts\n     * const data = YAML.parseAll(`\n     * ---\n     * id: 1\n     * name: Alice\n     * ---\n     * id: 2\n     * name: Bob\n     * ---\n     * id: 3\n     * name: Eve\n     * `);\n     *\n     * assertEquals(data, [ { id: 1, name: \"Alice\" }, { id: 2, name: \"Bob\" }, { id: 3, name: \"Eve\" }]);\n     * ```\n     *\n     * @param content YAML string to parse.\n     * @param options Parsing options.\n     * @returns Array of parsed documents.\n     */\n    parseAll: (content: string, options?: YamlParseOptions) => unknown;\n    /**\n     * Parse and return a YAML string as a Docker Compose file.\n     *\n     * @example Usage\n     * ```ts\n     * const stack = await komodo.read(\"GetStack\", { stack: \"test-stack\" });\n     * const contents = stack?.config?.file_contents;\n     *\n     * const parsed: DockerCompose = YAML.parseDockerCompose(contents)\n     * ```\n     *\n     * @throws {SyntaxError} Throws error on invalid YAML.\n     * @param content Docker compose file string.\n     * @param options Parsing options.\n     * @returns Parsed document.\n     */\n    parseDockerCompose: (\n      content: string,\n      options?: YamlParseOptions\n    ) => DockerCompose;\n  };\n  /** TOML parsing utilities */\n  var TOML: {\n    /**\n     * Converts an object to a [TOML string](https://toml.io).\n     *\n     * @example Usage\n     * ```ts\n     * const obj = {\n     *   title: \"TOML Example\",\n     *   owner: {\n     *     name: \"Bob\",\n     *     bio: \"Bob is a cool guy\",\n     *  }\n     * };\n     *\n     * const tomlString = TOML.stringify(obj);\n     *\n     * assertEquals(tomlString, `title = \"TOML Example\"\\n\\n[owner]\\nname = \"Bob\"\\nbio = \"Bob is a cool guy\"\\n`);\n     * ```\n     * @param obj Source object\n     * @param options Options for stringifying.\n     * @returns TOML string\n     */\n    stringify: (\n      obj: Record<string, unknown>,\n      options?: TomlStringifyOptions\n    ) => string;\n    /**\n     * Parses a [TOML string](https://toml.io) into an object.\n     *\n     * @example Usage\n     * ```ts\n     * const tomlString = `title = \"TOML Example\"\n     * [owner]\n     * name = \"Alice\"\n     * bio = \"Alice is a programmer.\"`;\n     *\n     * const obj = TOML.parse(tomlString);\n     *\n     * assertEquals(obj, { title: \"TOML Example\", owner: { name: \"Alice\", bio: \"Alice is a programmer.\" } });\n     * ```\n     * @param tomlString TOML string to be parsed.\n     * @returns The parsed JS object.\n     */\n    parse: (tomlString: string) => Record<string, unknown>;\n    /**\n     * Parses Komodo resource.toml contents to an object\n     * for easier handling.\n     *\n     * @example Usage\n     * ```ts\n     * const sync = await komodo.read(\"GetResourceSync\", { sync: \"test-sync\" })\n     * const contents = sync?.config?.file_contents;\n     *\n     * const resources: Types.ResourcesToml = TOML.parseResourceToml(contents);\n     * ```\n     *\n     * @param resourceToml The resource file contents.\n     * @returns Komodo resource.toml contents as JSON\n     */\n    parseResourceToml: (resourceToml: string) => Types.ResourcesToml;\n    /**\n     * Parses Cargo.toml contents to an object\n     * for easier handling.\n     *\n     * @example Usage\n     * ```ts\n     *  const contents = Deno.readTextFile(\"/path/to/Cargo.toml\");\n     * const cargoToml: CargoToml = TOML.parseCargoToml(contents);\n     * ```\n     *\n     * @param cargoToml The Cargo.toml contents.\n     * @returns Cargo.toml contents as JSON\n     */\n    parseCargoToml: (cargoToml: string) => CargoToml;\n  };\n}\n"
  },
  {
    "path": "frontend/public/manifest.json",
    "content": "{\n  \"name\": \"Komodo\",\n  \"short_name\": \"Komodo\",\n  \"icons\": [\n    {\n      \"src\": \"/komodo-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable\"\n    },\n    {\n      \"src\": \"/komodo-512x512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable\"\n    }\n  ],\n  \"theme_color\": \"#ffffff\",\n  \"background_color\": \"#000000\",\n  \"display\": \"standalone\"\n}"
  },
  {
    "path": "frontend/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "frontend/public/schema/compose-spec.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft-07/schema\",\n  \"$id\": \"compose_spec.json\",\n  \"type\": \"object\",\n  \"title\": \"Compose Specification\",\n  \"description\": \"The Compose file is a YAML file defining a multi-containers based application.\",\n\n  \"properties\": {\n    \"version\": {\n      \"type\": \"string\",\n      \"deprecated\": true,\n      \"description\": \"declared for backward compatibility, ignored. Please remove it.\"\n    },\n\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"define the Compose project name, until user defines one explicitly.\"\n    },\n\n    \"include\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/include\"\n      },\n      \"description\": \"compose sub-projects to be included.\"\n    },\n\n    \"services\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[a-zA-Z0-9._-]+$\": {\n          \"$ref\": \"#/definitions/service\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"description\": \"The services that will be used by your application.\"\n    },\n\n    \"models\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[a-zA-Z0-9._-]+$\": {\n          \"$ref\": \"#/definitions/model\"\n        }\n      },\n      \"description\": \"Language models that will be used by your application.\"\n    },\n\n\n    \"networks\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[a-zA-Z0-9._-]+$\": {\n          \"$ref\": \"#/definitions/network\"\n        }\n      },\n      \"description\": \"Networks that are shared among multiple services.\"\n    },\n\n    \"volumes\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[a-zA-Z0-9._-]+$\": {\n          \"$ref\": \"#/definitions/volume\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"description\": \"Named volumes that are shared among multiple services.\"\n    },\n\n    \"secrets\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[a-zA-Z0-9._-]+$\": {\n          \"$ref\": \"#/definitions/secret\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"description\": \"Secrets that are shared among multiple services.\"\n    },\n\n    \"configs\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[a-zA-Z0-9._-]+$\": {\n          \"$ref\": \"#/definitions/config\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"description\": \"Configurations that are shared among multiple services.\"\n    }\n  },\n\n  \"patternProperties\": {\"^x-\": {}},\n  \"additionalProperties\": false,\n\n  \"definitions\": {\n\n    \"service\": {\n      \"type\": \"object\",\n      \"description\": \"Configuration for a service.\",\n      \"properties\": {\n        \"develop\": {\"$ref\": \"#/definitions/development\"},\n        \"deploy\": {\"$ref\": \"#/definitions/deployment\"},\n        \"annotations\": {\"$ref\": \"#/definitions/list_or_dict\"},\n        \"attach\": {\"type\": [\"boolean\", \"string\"]},\n        \"build\": {\n          \"description\": \"Configuration options for building the service's image.\",\n          \"oneOf\": [\n            {\"type\": \"string\", \"description\": \"Path to the build context. Can be a relative path or a URL.\"},\n            {\n              \"type\": \"object\",\n              \"properties\": {\n                \"context\": {\"type\": \"string\", \"description\": \"Path to the build context. Can be a relative path or a URL.\"},\n                \"dockerfile\": {\"type\": \"string\", \"description\": \"Name of the Dockerfile to use for building the image.\"},\n                \"dockerfile_inline\": {\"type\": \"string\", \"description\": \"Inline Dockerfile content to use instead of a Dockerfile from the build context.\"},\n                \"entitlements\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"description\": \"List of extra privileged entitlements to grant to the build process.\"},\n                \"args\": {\"$ref\": \"#/definitions/list_or_dict\", \"description\": \"Build-time variables, specified as a map or a list of KEY=VAL pairs.\"},\n                \"ssh\": {\"$ref\": \"#/definitions/list_or_dict\", \"description\": \"SSH agent socket or keys to expose to the build. Format is either a string or a list of 'default|<id>[=<socket>|<key>[,<key>]]'.\"},\n                \"labels\": {\"$ref\": \"#/definitions/list_or_dict\", \"description\": \"Labels to apply to the built image.\"},\n                \"cache_from\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"description\": \"List of sources the image builder should use for cache resolution\"},\n                \"cache_to\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"description\": \"Cache destinations for the build cache.\"},\n                \"no_cache\": {\"type\": [\"boolean\", \"string\"], \"description\": \"Do not use cache when building the image.\"},\n                \"additional_contexts\": {\"$ref\": \"#/definitions/list_or_dict\", \"description\": \"Additional build contexts to use, specified as a map of name to context path or URL.\"},\n                \"network\": {\"type\": \"string\", \"description\": \"Network mode to use for the build. Options include 'default', 'none', 'host', or a network name.\"},\n                \"pull\": {\"type\": [\"boolean\", \"string\"], \"description\": \"Always attempt to pull a newer version of the image.\"},\n                \"target\": {\"type\": \"string\", \"description\": \"Build stage to target in a multi-stage Dockerfile.\"},\n                \"shm_size\": {\"type\": [\"integer\", \"string\"], \"description\": \"Size of /dev/shm for the build container. A string value can use suffix like '2g' for 2 gigabytes.\"},\n                \"extra_hosts\": {\"$ref\": \"#/definitions/extra_hosts\", \"description\": \"Add hostname mappings for the build container.\"},\n                \"isolation\": {\"type\": \"string\", \"description\": \"Container isolation technology to use for the build process.\"},\n                \"privileged\": {\"type\": [\"boolean\", \"string\"], \"description\": \"Give extended privileges to the build container.\"},\n                \"secrets\": {\"$ref\": \"#/definitions/service_config_or_secret\", \"description\": \"Secrets to expose to the build. These are accessible at build-time.\"},\n                \"tags\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"description\": \"Additional tags to apply to the built image.\"},\n                \"ulimits\": {\"$ref\": \"#/definitions/ulimits\", \"description\": \"Override the default ulimits for the build container.\"},\n                \"platforms\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"description\": \"Platforms to build for, e.g., 'linux/amd64', 'linux/arm64', or 'windows/amd64'.\"}\n              },\n              \"additionalProperties\": false,\n              \"patternProperties\": {\"^x-\": {}}\n            }\n          ]\n        },\n        \"blkio_config\": {\n          \"type\": \"object\",\n          \"description\": \"Block IO configuration for the service.\",\n          \"properties\": {\n            \"device_read_bps\": {\n              \"type\": \"array\",\n              \"description\": \"Limit read rate (bytes per second) from a device.\",\n              \"items\": {\"$ref\": \"#/definitions/blkio_limit\"}\n            },\n            \"device_read_iops\": {\n              \"type\": \"array\",\n              \"description\": \"Limit read rate (IO per second) from a device.\",\n              \"items\": {\"$ref\": \"#/definitions/blkio_limit\"}\n            },\n            \"device_write_bps\": {\n              \"type\": \"array\",\n              \"description\": \"Limit write rate (bytes per second) to a device.\",\n              \"items\": {\"$ref\": \"#/definitions/blkio_limit\"}\n            },\n            \"device_write_iops\": {\n              \"type\": \"array\",\n              \"description\": \"Limit write rate (IO per second) to a device.\",\n              \"items\": {\"$ref\": \"#/definitions/blkio_limit\"}\n            },\n            \"weight\": {\n              \"type\": [\"integer\", \"string\"],\n              \"description\": \"Block IO weight (relative weight) for the service, between 10 and 1000.\"\n            },\n            \"weight_device\": {\n              \"type\": \"array\",\n              \"description\": \"Block IO weight (relative weight) for specific devices.\",\n              \"items\": {\"$ref\": \"#/definitions/blkio_weight\"}\n            }\n          },\n          \"additionalProperties\": false\n        },\n        \"cap_add\": {\n          \"type\": \"array\",\n          \"items\": {\"type\": \"string\"},\n          \"uniqueItems\": true,\n          \"description\": \"Add Linux capabilities. For example, 'CAP_SYS_ADMIN', 'SYS_ADMIN', or 'NET_ADMIN'.\"\n        },\n        \"cap_drop\": {\n          \"type\": \"array\",\n          \"items\": {\"type\": \"string\"},\n          \"uniqueItems\": true,\n          \"description\": \"Drop Linux capabilities. For example, 'CAP_SYS_ADMIN', 'SYS_ADMIN', or 'NET_ADMIN'.\"\n        },\n        \"cgroup\": {\n          \"type\": \"string\",\n          \"enum\": [\"host\", \"private\"],\n          \"description\": \"Specify the cgroup namespace to join. Use 'host' to use the host's cgroup namespace, or 'private' to use a private cgroup namespace.\"\n        },\n        \"cgroup_parent\": {\n          \"type\": \"string\",\n          \"description\": \"Specify an optional parent cgroup for the container.\"\n        },\n        \"command\": {\n          \"$ref\": \"#/definitions/command\",\n          \"description\": \"Override the default command declared by the container image, for example 'CMD' in Dockerfile.\"\n        },\n        \"configs\": {\n          \"$ref\": \"#/definitions/service_config_or_secret\",\n          \"description\": \"Grant access to Configs on a per-service basis.\"\n        },\n        \"container_name\": {\n          \"type\": \"string\",\n          \"description\": \"Specify a custom container name, rather than a generated default name.\",\n          \"pattern\": \"[a-zA-Z0-9][a-zA-Z0-9_.-]+\"\n        },\n        \"cpu_count\": {\n          \"oneOf\": [\n            {\"type\": \"string\"},\n            {\"type\": \"integer\", \"minimum\": 0}\n          ],\n          \"description\": \"Number of usable CPUs.\"\n        },\n        \"cpu_percent\": {\n          \"oneOf\": [\n            {\"type\": \"string\"},\n            {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 100}\n          ],\n          \"description\": \"Percentage of CPU resources to use.\"\n        },\n        \"cpu_shares\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"CPU shares (relative weight) for the container.\"\n        },\n        \"cpu_quota\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"Limit the CPU CFS (Completely Fair Scheduler) quota.\"\n        },\n        \"cpu_period\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"Limit the CPU CFS (Completely Fair Scheduler) period.\"\n        },\n        \"cpu_rt_period\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"Limit the CPU real-time period in microseconds or a duration.\"\n        },\n        \"cpu_rt_runtime\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"Limit the CPU real-time runtime in microseconds or a duration.\"\n        },\n        \"cpus\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"Number of CPUs to use. A floating-point value is supported to request partial CPUs.\"\n        },\n        \"cpuset\": {\n          \"type\": \"string\",\n          \"description\": \"CPUs in which to allow execution (0-3, 0,1).\"\n        },\n        \"credential_spec\": {\n          \"type\": \"object\",\n          \"description\": \"Configure the credential spec for managed service account.\",\n          \"properties\": {\n            \"config\": {\n              \"type\": \"string\",\n              \"description\": \"The name of the credential spec Config to use.\"\n            },\n            \"file\": {\n              \"type\": \"string\",\n              \"description\": \"Path to a credential spec file.\"\n            },\n            \"registry\": {\n              \"type\": \"string\",\n              \"description\": \"Path to a credential spec in the Windows registry.\"\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        },\n        \"depends_on\": {\n          \"oneOf\": [\n            {\"$ref\": \"#/definitions/list_of_strings\"},\n            {\n              \"type\": \"object\",\n              \"additionalProperties\": false,\n              \"patternProperties\": {\n                \"^[a-zA-Z0-9._-]+$\": {\n                  \"type\": \"object\",\n                  \"additionalProperties\": false,\n                  \"patternProperties\": {\"^x-\": {}},\n                  \"properties\": {\n                    \"restart\": {\n                      \"type\": [\"boolean\", \"string\"],\n                      \"description\": \"Whether to restart dependent services when this service is restarted.\"\n                    },\n                    \"required\": {\n                      \"type\":  \"boolean\",\n                      \"default\": true,\n                      \"description\": \"Whether the dependency is required for the dependent service to start.\"\n                    },\n                    \"condition\": {\n                      \"type\": \"string\",\n                      \"enum\": [\"service_started\", \"service_healthy\", \"service_completed_successfully\"],\n                      \"description\": \"Condition to wait for. 'service_started' waits until the service has started, 'service_healthy' waits until the service is healthy (as defined by its healthcheck), 'service_completed_successfully' waits until the service has completed successfully.\"\n                    }\n                  },\n                  \"required\": [\"condition\"]\n                }\n              }\n            }\n          ],\n          \"description\": \"Express dependency between services. Service dependencies cause services to be started in dependency order. The dependent service will wait for the dependency to be ready before starting.\"\n        },\n        \"device_cgroup_rules\": {\n          \"$ref\": \"#/definitions/list_of_strings\",\n          \"description\": \"Add rules to the cgroup allowed devices list.\"\n        },\n        \"devices\": {\n          \"type\": \"array\",\n          \"description\": \"List of device mappings for the container.\",\n          \"items\": {\n            \"oneOf\": [\n              {\"type\": \"string\"},\n              {\n                \"type\": \"object\",\n                \"required\": [\"source\"],\n                \"properties\": {\n                  \"source\": {\n                    \"type\": \"string\",\n                    \"description\": \"Path on the host to the device.\"\n                  },\n                  \"target\": {\n                    \"type\": \"string\",\n                    \"description\": \"Path in the container where the device will be mapped.\"\n                  },\n                  \"permissions\": {\n                    \"type\": \"string\",\n                    \"description\": \"Cgroup permissions for the device (rwm).\"\n                  }\n                },\n                \"additionalProperties\": false,\n                \"patternProperties\": {\"^x-\": {}}\n              }\n            ]\n          }\n        },\n        \"dns\": {\n          \"$ref\": \"#/definitions/string_or_list\",\n          \"description\": \"Custom DNS servers to set for the service container.\"\n        },\n        \"dns_opt\": {\n          \"type\": \"array\",\n          \"items\": {\"type\": \"string\"},\n          \"uniqueItems\": true,\n          \"description\": \"Custom DNS options to be passed to the container's DNS resolver.\"\n        },\n        \"dns_search\": {\n          \"$ref\": \"#/definitions/string_or_list\",\n          \"description\": \"Custom DNS search domains to set on the service container.\"\n        },\n        \"domainname\": {\n          \"type\": \"string\",\n          \"description\": \"Custom domain name to use for the service container.\"\n        },\n        \"entrypoint\": {\n          \"$ref\": \"#/definitions/command\",\n          \"description\": \"Override the default entrypoint declared by the container image, for example 'ENTRYPOINT' in Dockerfile.\"\n        },\n        \"env_file\": {\n          \"$ref\": \"#/definitions/env_file\",\n          \"description\": \"Add environment variables from a file or multiple files. Can be a single file path or a list of file paths.\"\n        },\n        \"label_file\": {\n          \"$ref\": \"#/definitions/label_file\",\n          \"description\": \"Add metadata to containers using files containing Docker labels.\"\n        },\n        \"environment\": {\n          \"$ref\": \"#/definitions/list_or_dict\",\n          \"description\": \"Add environment variables. You can use either an array or a list of KEY=VAL pairs.\"\n        },\n        \"expose\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": [\"string\", \"number\"]\n          },\n          \"uniqueItems\": true,\n          \"description\": \"Expose ports without publishing them to the host machine - they'll only be accessible to linked services.\"\n        },\n        \"extends\": {\n          \"oneOf\": [\n            {\"type\": \"string\"},\n            {\n              \"type\": \"object\",\n              \"properties\": {\n                \"service\": {\n                  \"type\": \"string\",\n                  \"description\": \"The name of the service to extend.\"\n                },\n                \"file\": {\n                  \"type\": \"string\",\n                  \"description\": \"The file path where the service to extend is defined.\"\n                }\n              },\n              \"required\": [\"service\"],\n              \"additionalProperties\": false\n            }\n          ],\n          \"description\": \"Extend another service, in the current file or another file.\"\n        },\n        \"provider\": {\n          \"type\": \"object\",\n          \"description\": \"Specify a service which will not be manage by Compose directly, and delegate its management to an external provider.\",\n          \"required\": [\"type\"],\n          \"properties\": {\n            \"type\": {\n              \"type\": \"string\",\n              \"description\": \"External component used by Compose to manage setup and teardown lifecycle of the service.\"\n            },\n            \"options\": {\n              \"type\": \"object\",\n              \"description\": \"Provider-specific options.\",\n              \"patternProperties\": {\n                \"^.+$\": {\"oneOf\": [\n                  { \"type\": [\"string\", \"number\", \"boolean\"] },\n                  { \"type\": \"array\", \"items\": {\"type\": [\"string\", \"number\", \"boolean\"]}}\n                ]}\n              }\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        },\n        \"external_links\": {\n          \"type\": \"array\",\n          \"items\": {\"type\": \"string\"},\n          \"uniqueItems\": true,\n          \"description\": \"Link to services started outside this Compose application. Specify services as <service_name>:<alias>.\"\n        },\n        \"extra_hosts\": {\n          \"$ref\": \"#/definitions/extra_hosts\",\n          \"description\": \"Add hostname mappings to the container network interface configuration.\"\n        },\n        \"gpus\": {\n          \"$ref\": \"#/definitions/gpus\",\n          \"description\": \"Define GPU devices to use. Can be set to 'all' to use all GPUs, or a list of specific GPU devices.\"\n        },\n        \"group_add\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": [\"string\", \"number\"]\n          },\n          \"uniqueItems\": true,\n          \"description\": \"Add additional groups which user inside the container should be member of.\"\n        },\n        \"healthcheck\": {\n          \"$ref\": \"#/definitions/healthcheck\",\n          \"description\": \"Configure a health check for the container to monitor its health status.\"\n        },\n        \"hostname\": {\n          \"type\": \"string\",\n          \"description\": \"Define a custom hostname for the service container.\"\n        },\n        \"image\": {\n          \"type\": \"string\",\n          \"description\": \"Specify the image to start the container from. Can be a repository/tag, a digest, or a local image ID.\"\n        },\n        \"init\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Run as an init process inside the container that forwards signals and reaps processes.\"\n        },\n        \"ipc\": {\n          \"type\": \"string\",\n          \"description\": \"IPC sharing mode for the service container. Use 'host' to share the host's IPC namespace, 'service:[service_name]' to share with another service, or 'shareable' to allow other services to share this service's IPC namespace.\"\n        },\n        \"isolation\": {\n          \"type\": \"string\",\n          \"description\": \"Container isolation technology to use. Supported values are platform-specific.\"\n        },\n        \"labels\": {\n          \"$ref\": \"#/definitions/list_or_dict\",\n          \"description\": \"Add metadata to containers using Docker labels. You can use either an array or a list.\"\n        },\n        \"links\": {\n          \"type\": \"array\",\n          \"items\": {\"type\": \"string\"},\n          \"uniqueItems\": true,\n          \"description\": \"Link to containers in another service. Either specify both the service name and a link alias (SERVICE:ALIAS), or just the service name.\"\n        },\n        \"logging\": {\n          \"type\": \"object\",\n          \"description\": \"Logging configuration for the service.\",\n          \"properties\": {\n            \"driver\": {\n              \"type\": \"string\",\n              \"description\": \"Logging driver to use, such as 'json-file', 'syslog', 'journald', etc.\"\n            },\n            \"options\": {\n              \"type\": \"object\",\n              \"description\": \"Options for the logging driver.\",\n              \"patternProperties\": {\n                \"^.+$\": {\"type\": [\"string\", \"number\", \"null\"]}\n              }\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        },\n        \"mac_address\": {\n          \"type\": \"string\",\n          \"description\": \"Container MAC address to set.\"\n        },\n        \"mem_limit\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"Memory limit for the container. A string value can use suffix like '2g' for 2 gigabytes.\"\n        },\n        \"mem_reservation\": {\n          \"type\": [\"string\", \"integer\"],\n          \"description\": \"Memory reservation for the container.\"\n        },\n        \"mem_swappiness\": {\n          \"type\": [\"integer\", \"string\"],\n          \"description\": \"Container memory swappiness as percentage (0 to 100).\"\n        },\n        \"memswap_limit\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"Amount of memory the container is allowed to swap to disk. Set to -1 to enable unlimited swap.\"\n        },\n        \"network_mode\": {\n          \"type\": \"string\",\n          \"description\": \"Network mode. Values can be 'bridge', 'host', 'none', 'service:[service name]', or 'container:[container name]'.\"\n        },\n        \"models\": {\n          \"oneOf\": [\n            {\"$ref\": \"#/definitions/list_of_strings\"},\n            {\"type\": \"object\",\n              \"patternProperties\": {\n                \"^[a-zA-Z0-9._-]+$\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"endpoint_var\": {\n                      \"type\": \"string\",\n                      \"description\": \"Environment variable set to AI model endpoint.\"\n                    },\n                    \"model_var\": {\n                      \"type\": \"string\",\n                      \"description\": \"Environment variable set to AI model name.\"\n                    }\n                  },\n                  \"additionalProperties\": false,\n                  \"patternProperties\": {\"^x-\": {}}\n                }\n              }\n            }\n          ],\n          \"description\": \"AI Models to use, referencing entries under the top-level models key.\"\n        },\n        \"networks\": {\n          \"oneOf\": [\n            {\"$ref\": \"#/definitions/list_of_strings\"},\n            {\n              \"type\": \"object\",\n              \"patternProperties\": {\n                \"^[a-zA-Z0-9._-]+$\": {\n                  \"oneOf\": [\n                    {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"aliases\": {\n                          \"$ref\": \"#/definitions/list_of_strings\",\n                          \"description\": \"Alternative hostnames for this service on the network.\"\n                        },\n                        \"interface_name\": {\n                          \"type\": \"string\",\n                          \"description\": \"Interface network name used to connect to network\"\n                        },\n                        \"ipv4_address\": {\n                          \"type\": \"string\",\n                          \"description\": \"Specify a static IPv4 address for this service on this network.\"\n                        },\n                        \"ipv6_address\": {\n                          \"type\": \"string\",\n                          \"description\": \"Specify a static IPv6 address for this service on this network.\"\n                        },\n                        \"link_local_ips\": {\n                          \"$ref\": \"#/definitions/list_of_strings\",\n                          \"description\": \"List of link-local IPs.\"\n                        },\n                        \"mac_address\": {\n                          \"type\": \"string\",\n                          \"description\": \"Specify a MAC address for this service on this network.\"\n                        },\n                        \"driver_opts\": {\n                          \"type\": \"object\",\n                          \"description\": \"Driver options for this network.\",\n                          \"patternProperties\": {\n                            \"^.+$\": {\"type\": [\"string\", \"number\"]}\n                          }\n                        },\n                        \"priority\": {\n                          \"type\": \"number\",\n                          \"description\": \"Specify the priority for the network connection.\"\n                        },\n                        \"gw_priority\": {\n                          \"type\": \"number\",\n                          \"description\": \"Specify the gateway priority for the network connection.\"\n                        }\n                      },\n                      \"additionalProperties\": false,\n                      \"patternProperties\": {\"^x-\": {}}\n                    },\n                    {\"type\": \"null\"}\n                  ]\n                }\n              },\n              \"additionalProperties\": false\n            }\n          ],\n          \"description\": \"Networks to join, referencing entries under the top-level networks key. Can be a list of network names or a mapping of network name to network configuration.\"\n        },\n        \"oom_kill_disable\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Disable OOM Killer for the container.\"\n        },\n        \"oom_score_adj\": {\n          \"oneOf\": [\n            {\"type\": \"string\"},\n            {\"type\": \"integer\", \"minimum\": -1000, \"maximum\": 1000}\n          ],\n          \"description\": \"Tune host's OOM preferences for the container (accepts -1000 to 1000).\"\n        },\n        \"pid\": {\n          \"type\": [\"string\", \"null\"],\n          \"description\": \"PID mode for container.\"\n        },\n        \"pids_limit\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"Tune a container's PIDs limit. Set to -1 for unlimited PIDs.\"\n        },\n        \"platform\": {\n          \"type\": \"string\",\n          \"description\": \"Target platform to run on, e.g., 'linux/amd64', 'linux/arm64', or 'windows/amd64'.\"\n        },\n        \"ports\": {\n          \"type\": \"array\",\n          \"description\": \"Expose container ports. Short format ([HOST:]CONTAINER[/PROTOCOL]).\",\n          \"items\": {\n            \"oneOf\": [\n              {\"type\": \"number\"},\n              {\"type\": \"string\"},\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"description\": \"A human-readable name for this port mapping.\"\n                  },\n                  \"mode\": {\n                    \"type\": \"string\",\n                    \"description\": \"The port binding mode, either 'host' for publishing a host port or 'ingress' for load balancing.\"\n                  },\n                  \"host_ip\": {\n                    \"type\": \"string\",\n                    \"description\": \"The host IP to bind to.\"\n                  },\n                  \"target\": {\n                    \"type\": [\"integer\", \"string\"],\n                    \"description\": \"The port inside the container.\"\n                  },\n                  \"published\": {\n                    \"type\": [\"string\", \"integer\"],\n                    \"description\": \"The publicly exposed port.\"\n                  },\n                  \"protocol\": {\n                    \"type\": \"string\",\n                    \"description\": \"The port protocol (tcp or udp).\"\n                  },\n                  \"app_protocol\": {\n                    \"type\": \"string\",\n                    \"description\": \"Application protocol to use with the port (e.g., http, https, mysql).\"\n                  }\n                },\n                \"additionalProperties\": false,\n                \"patternProperties\": {\"^x-\": {}}\n              }\n            ]\n          },\n          \"uniqueItems\": true\n        },\n        \"post_start\": {\n          \"type\": \"array\",\n          \"items\": {\"$ref\": \"#/definitions/service_hook\"},\n          \"description\": \"Commands to run after the container starts. If any command fails, the container stops.\"\n        },\n        \"pre_stop\": {\n          \"type\": \"array\",\n          \"items\": {\"$ref\": \"#/definitions/service_hook\"},\n          \"description\": \"Commands to run before the container stops. If any command fails, the container stop is aborted.\"\n        },\n        \"privileged\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Give extended privileges to the service container.\"\n        },\n        \"profiles\": {\n          \"$ref\": \"#/definitions/list_of_strings\",\n          \"description\": \"List of profiles for this service. When profiles are specified, services are only started when the profile is activated.\"\n        },\n        \"pull_policy\": {\n          \"type\": \"string\",\n          \"pattern\": \"always|never|build|if_not_present|missing|refresh|daily|weekly|every_([0-9]+[wdhms])+\",\n          \"description\": \"Policy for pulling images. Options include: 'always', 'never', 'if_not_present', 'missing', 'build', or time-based refresh policies.\"\n        },\n        \"pull_refresh_after\": {\n          \"type\": \"string\",\n          \"description\": \"Time after which to refresh the image. Used with pull_policy=refresh.\"\n        },\n        \"read_only\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Mount the container's filesystem as read only.\"\n        },\n        \"restart\": {\n          \"type\": \"string\",\n          \"description\": \"Restart policy for the service container. Options include: 'no', 'always', 'on-failure', and 'unless-stopped'.\"\n        },\n        \"runtime\": {\n          \"type\": \"string\",\n          \"description\": \"Runtime to use for this container, e.g., 'runc'.\"\n        },\n        \"scale\": {\n          \"type\": [\"integer\", \"string\"],\n          \"description\": \"Number of containers to deploy for this service.\"\n        },\n        \"security_opt\": {\n          \"type\": \"array\",\n          \"items\": {\"type\": \"string\"},\n          \"uniqueItems\": true,\n          \"description\": \"Override the default labeling scheme for each container.\"\n        },\n        \"shm_size\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"Size of /dev/shm. A string value can use suffix like '2g' for 2 gigabytes.\"\n        },\n        \"secrets\": {\n          \"$ref\": \"#/definitions/service_config_or_secret\",\n          \"description\": \"Grant access to Secrets on a per-service basis.\"\n        },\n        \"sysctls\": {\n          \"$ref\": \"#/definitions/list_or_dict\",\n          \"description\": \"Kernel parameters to set in the container. You can use either an array or a list.\"\n        },\n        \"stdin_open\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Keep STDIN open even if not attached.\"\n        },\n        \"stop_grace_period\": {\n          \"type\": \"string\",\n          \"description\": \"Time to wait for the container to stop gracefully before sending SIGKILL (e.g., '1s', '1m30s').\"\n        },\n        \"stop_signal\": {\n          \"type\": \"string\",\n          \"description\": \"Signal to stop the container (e.g., 'SIGTERM', 'SIGINT').\"\n        },\n        \"storage_opt\": {\n          \"type\": \"object\",\n          \"description\": \"Storage driver options for the container.\"\n        },\n        \"tmpfs\": {\n          \"$ref\": \"#/definitions/string_or_list\",\n          \"description\": \"Mount a temporary filesystem (tmpfs) into the container. Can be a single value or a list.\"\n        },\n        \"tty\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Allocate a pseudo-TTY to service container.\"\n        },\n        \"ulimits\": {\n          \"$ref\": \"#/definitions/ulimits\",\n          \"description\": \"Override the default ulimits for a container.\"\n        },\n        \"use_api_socket\": {\n          \"type\": \"boolean\",\n          \"description\": \"Bind mount Docker API socket and required auth.\"\n        },\n        \"user\": {\n          \"type\": \"string\",\n          \"description\": \"Username or UID to run the container process as.\"\n        },\n        \"uts\": {\n          \"type\": \"string\",\n          \"description\": \"UTS namespace to use. 'host' shares the host's UTS namespace.\"\n        },\n        \"userns_mode\": {\n          \"type\": \"string\",\n          \"description\": \"User namespace to use. 'host' shares the host's user namespace.\"\n        },\n        \"volumes\": {\n          \"type\": \"array\",\n          \"description\": \"Mount host paths or named volumes accessible to the container. Short syntax (VOLUME:CONTAINER_PATH[:MODE])\",\n          \"items\": {\n            \"oneOf\": [\n              {\"type\": \"string\"},\n              {\n                \"type\": \"object\",\n                \"required\": [\"type\"],\n                \"properties\": {\n                  \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"bind\", \"volume\", \"tmpfs\", \"cluster\", \"npipe\", \"image\"],\n                    \"description\": \"The mount type: bind for mounting host directories, volume for named volumes, tmpfs for temporary filesystems, cluster for cluster volumes, npipe for named pipes, or image for mounting from an image.\"\n                  },\n                  \"source\": {\n                    \"type\": \"string\",\n                    \"description\": \"The source of the mount, a path on the host for a bind mount, a docker image reference for an image mount, or the name of a volume defined in the top-level volumes key. Not applicable for a tmpfs mount.\"\n                  },\n                  \"target\": {\n                    \"type\": \"string\",\n                    \"description\": \"The path in the container where the volume is mounted.\"\n                  },\n                  \"read_only\": {\n                    \"type\": [\"boolean\", \"string\"],\n                    \"description\": \"Flag to set the volume as read-only.\"\n                  },\n                  \"consistency\": {\n                    \"type\": \"string\",\n                    \"description\": \"The consistency requirements for the mount. Available values are platform specific.\"\n                  },\n                  \"bind\": {\n                    \"type\": \"object\",\n                    \"description\": \"Configuration specific to bind mounts.\",\n                    \"properties\": {\n                      \"propagation\": {\n                        \"type\": \"string\",\n                        \"description\": \"The propagation mode for the bind mount: 'shared', 'slave', 'private', 'rshared', 'rslave', or 'rprivate'.\"\n                      },\n                      \"create_host_path\": {\n                        \"type\": [\"boolean\", \"string\"],\n                        \"description\": \"Create the host path if it doesn't exist.\"\n                      },\n                      \"recursive\": {\n                        \"type\": \"string\",\n                        \"enum\": [\"enabled\", \"disabled\", \"writable\", \"readonly\"],\n                        \"description\": \"Recursively mount the source directory.\"\n                      },\n                      \"selinux\": {\n                        \"type\": \"string\",\n                        \"enum\": [\"z\", \"Z\"],\n                        \"description\": \"SELinux relabeling options: 'z' for shared content, 'Z' for private unshared content.\"\n                      }\n                    },\n                    \"additionalProperties\": false,\n                    \"patternProperties\": {\"^x-\": {}}\n                  },\n                  \"volume\": {\n                    \"type\": \"object\",\n                    \"description\": \"Configuration specific to volume mounts.\",\n                    \"properties\": {\n                      \"labels\": {\n                        \"$ref\": \"#/definitions/list_or_dict\",\n                        \"description\": \"Labels to apply to the volume.\"\n                      },\n                      \"nocopy\": {\n                        \"type\": [\"boolean\", \"string\"],\n                        \"description\": \"Flag to disable copying of data from a container when a volume is created.\"\n                      },\n                      \"subpath\": {\n                        \"type\": \"string\",\n                        \"description\": \"Path within the volume to mount instead of the volume root.\"\n                      }\n                    },\n                    \"additionalProperties\": false,\n                    \"patternProperties\": {\"^x-\": {}}\n                  },\n                  \"tmpfs\": {\n                    \"type\": \"object\",\n                    \"description\": \"Configuration specific to tmpfs mounts.\",\n                    \"properties\": {\n                      \"size\": {\n                        \"oneOf\": [\n                          {\"type\": \"integer\", \"minimum\": 0},\n                          {\"type\": \"string\"}\n                        ],\n                        \"description\": \"Size of the tmpfs mount in bytes.\"\n                      },\n                      \"mode\": {\n                        \"type\": [\"number\", \"string\"],\n                        \"description\": \"File mode of the tmpfs in octal.\"\n                      }\n                    },\n                    \"additionalProperties\": false,\n                    \"patternProperties\": {\"^x-\": {}}\n                  },\n                  \"image\": {\n                    \"type\": \"object\",\n                    \"description\": \"Configuration specific to image mounts.\",\n                    \"properties\": {\n                      \"subpath\": {\n                        \"type\": \"string\",\n                        \"description\": \"Path within the image to mount instead of the image root.\"\n                      }\n                    },\n                    \"additionalProperties\": false,\n                    \"patternProperties\": {\"^x-\": {}}\n                  }\n                },\n                \"additionalProperties\": false,\n                \"patternProperties\": {\"^x-\": {}}\n              }\n            ]\n          },\n          \"uniqueItems\": true\n        },\n        \"volumes_from\": {\n          \"type\": \"array\",\n          \"items\": {\"type\": \"string\"},\n          \"uniqueItems\": true,\n          \"description\": \"Mount volumes from another service or container. Optionally specify read-only access (ro) or read-write (rw).\"\n        },\n        \"working_dir\": {\n          \"type\": \"string\",\n          \"description\": \"The working directory in which the entrypoint or command will be run\"\n        }\n      },\n      \"patternProperties\": {\"^x-\": {}},\n      \"additionalProperties\": false\n    },\n\n    \"healthcheck\": {\n      \"type\": \"object\",\n      \"description\": \"Configuration options to determine whether the container is healthy.\",\n      \"properties\": {\n        \"disable\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Disable any container-specified healthcheck. Set to true to disable.\"\n        },\n        \"interval\": {\n          \"type\": \"string\",\n          \"description\": \"Time between running the check (e.g., '1s', '1m30s'). Default: 30s.\"\n        },\n        \"retries\": {\n          \"type\": [\"number\", \"string\"],\n          \"description\": \"Number of consecutive failures needed to consider the container as unhealthy. Default: 3.\"\n        },\n        \"test\": {\n          \"oneOf\": [\n            {\"type\": \"string\"},\n            {\"type\": \"array\", \"items\": {\"type\": \"string\"}}\n          ],\n          \"description\": \"The test to perform to check container health. Can be a string or a list. The first item is either NONE, CMD, or CMD-SHELL. If it's CMD, the rest of the command is exec'd. If it's CMD-SHELL, the rest is run in the shell.\"\n        },\n        \"timeout\": {\n          \"type\": \"string\",\n          \"description\": \"Maximum time to allow one check to run (e.g., '1s', '1m30s'). Default: 30s.\"\n        },\n        \"start_period\": {\n          \"type\": \"string\",\n          \"description\": \"Start period for the container to initialize before starting health-retries countdown (e.g., '1s', '1m30s'). Default: 0s.\"\n        },\n        \"start_interval\": {\n          \"type\": \"string\",\n          \"description\": \"Time between running the check during the start period (e.g., '1s', '1m30s'). Default: interval value.\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"patternProperties\": {\"^x-\": {}}\n    },\n    \"development\": {\n      \"type\": [\"object\", \"null\"],\n      \"description\": \"Development configuration for the service, used for development workflows.\",\n      \"properties\": {\n        \"watch\": {\n          \"type\": \"array\",\n          \"description\": \"Configure watch mode for the service, which monitors file changes and performs actions in response.\",\n          \"items\": {\n            \"type\": \"object\",\n            \"required\": [\"path\", \"action\"],\n            \"properties\": {\n              \"ignore\": {\n                \"$ref\": \"#/definitions/string_or_list\",\n                \"description\": \"Patterns to exclude from watching.\"\n              },\n              \"include\": {\n                \"$ref\": \"#/definitions/string_or_list\",\n                \"description\": \"Patterns to include in watching.\"\n              },\n              \"path\": {\n                \"type\": \"string\",\n                \"description\": \"Path to watch for changes.\"\n              },\n              \"action\": {\n                \"type\": \"string\",\n                \"enum\": [\"rebuild\", \"sync\", \"restart\", \"sync+restart\", \"sync+exec\"],\n                \"description\": \"Action to take when a change is detected: rebuild the container, sync files, restart the container, sync and restart, or sync and execute a command.\"\n              },\n              \"target\": {\n                \"type\": \"string\",\n                \"description\": \"Target path in the container for sync operations.\"\n              },\n              \"exec\": {\n                \"$ref\": \"#/definitions/service_hook\",\n                \"description\": \"Command to execute when a change is detected and action is sync+exec.\"\n              }\n            },\n            \"additionalProperties\": false,\n            \"patternProperties\": {\"^x-\": {}}\n          }\n        }\n      },\n      \"additionalProperties\": false,\n      \"patternProperties\": {\"^x-\": {}}\n    },\n    \"deployment\": {\n      \"type\": [\"object\", \"null\"],\n      \"description\": \"Deployment configuration for the service.\",\n      \"properties\": {\n        \"mode\": {\n          \"type\": \"string\",\n          \"description\": \"Deployment mode for the service: 'replicated' (default) or 'global'.\"\n        },\n        \"endpoint_mode\": {\n          \"type\": \"string\",\n          \"description\": \"Endpoint mode for the service: 'vip' (default) or 'dnsrr'.\"\n        },\n        \"replicas\": {\n          \"type\": [\"integer\", \"string\"],\n          \"description\": \"Number of replicas of the service container to run.\"\n        },\n        \"labels\": {\n          \"$ref\": \"#/definitions/list_or_dict\",\n          \"description\": \"Labels to apply to the service.\"\n        },\n        \"rollback_config\": {\n          \"type\": \"object\",\n          \"description\": \"Configuration for rolling back a service update.\",\n          \"properties\": {\n            \"parallelism\": {\n              \"type\": [\"integer\", \"string\"],\n              \"description\": \"The number of containers to rollback at a time. If set to 0, all containers rollback simultaneously.\"\n            },\n            \"delay\": {\n              \"type\": \"string\",\n              \"description\": \"The time to wait between each container group's rollback (e.g., '1s', '1m30s').\"\n            },\n            \"failure_action\": {\n              \"type\": \"string\",\n              \"description\": \"Action to take if a rollback fails: 'continue', 'pause'.\"\n            },\n            \"monitor\": {\n              \"type\": \"string\",\n              \"description\": \"Duration to monitor each task for failures after it is created (e.g., '1s', '1m30s').\"\n            },\n            \"max_failure_ratio\": {\n              \"type\": [\"number\", \"string\"],\n              \"description\": \"Failure rate to tolerate during a rollback.\"\n            },\n            \"order\": {\n              \"type\": \"string\",\n              \"enum\": [\"start-first\", \"stop-first\"],\n              \"description\": \"Order of operations during rollbacks: 'stop-first' (default) or 'start-first'.\"\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        },\n        \"update_config\": {\n          \"type\": \"object\",\n          \"description\": \"Configuration for updating a service.\",\n          \"properties\": {\n            \"parallelism\": {\n              \"type\": [\"integer\", \"string\"],\n              \"description\": \"The number of containers to update at a time.\"\n            },\n            \"delay\": {\n              \"type\": \"string\",\n              \"description\": \"The time to wait between updating a group of containers (e.g., '1s', '1m30s').\"\n            },\n            \"failure_action\": {\n              \"type\": \"string\",\n              \"description\": \"Action to take if an update fails: 'continue', 'pause', 'rollback'.\"\n            },\n            \"monitor\": {\n              \"type\": \"string\",\n              \"description\": \"Duration to monitor each updated task for failures after it is created (e.g., '1s', '1m30s').\"\n            },\n            \"max_failure_ratio\": {\n              \"type\": [\"number\", \"string\"],\n              \"description\": \"Failure rate to tolerate during an update (0 to 1).\"\n            },\n            \"order\": {\n              \"type\": \"string\",\n              \"enum\": [\"start-first\", \"stop-first\"],\n              \"description\": \"Order of operations during updates: 'stop-first' (default) or 'start-first'.\"\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        },\n        \"resources\": {\n          \"type\": \"object\",\n          \"description\": \"Resource constraints and reservations for the service.\",\n          \"properties\": {\n            \"limits\": {\n              \"type\": \"object\",\n              \"description\": \"Resource limits for the service containers.\",\n              \"properties\": {\n                \"cpus\": {\n                  \"type\": [\"number\", \"string\"],\n                  \"description\": \"Limit for how much of the available CPU resources, as number of cores, a container can use.\"\n                },\n                \"memory\": {\n                  \"type\": \"string\",\n                  \"description\": \"Limit on the amount of memory a container can allocate (e.g., '1g', '1024m').\"\n                },\n                \"pids\": {\n                  \"type\": [\"integer\", \"string\"],\n                  \"description\": \"Maximum number of PIDs available to the container.\"\n                }\n              },\n              \"additionalProperties\": false,\n              \"patternProperties\": {\"^x-\": {}}\n            },\n            \"reservations\": {\n              \"type\": \"object\",\n              \"description\": \"Resource reservations for the service containers.\",\n              \"properties\": {\n                \"cpus\": {\n                  \"type\": [\"number\", \"string\"],\n                  \"description\": \"Reservation for how much of the available CPU resources, as number of cores, a container can use.\"\n                },\n                \"memory\": {\n                  \"type\": \"string\",\n                  \"description\": \"Reservation on the amount of memory a container can allocate (e.g., '1g', '1024m').\"\n                },\n                \"generic_resources\": {\n                  \"$ref\": \"#/definitions/generic_resources\",\n                  \"description\": \"User-defined resources to reserve.\"\n                },\n                \"devices\": {\n                  \"$ref\": \"#/definitions/devices\",\n                  \"description\": \"Device reservations for the container.\"\n                }\n              },\n              \"additionalProperties\": false,\n              \"patternProperties\": {\"^x-\": {}}\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        },\n        \"restart_policy\": {\n          \"type\": \"object\",\n          \"description\": \"Restart policy for the service containers.\",\n          \"properties\": {\n            \"condition\": {\n              \"type\": \"string\",\n              \"description\": \"Condition for restarting the container: 'none', 'on-failure', 'any'.\"\n            },\n            \"delay\": {\n              \"type\": \"string\",\n              \"description\": \"Delay between restart attempts (e.g., '1s', '1m30s').\"\n            },\n            \"max_attempts\": {\n              \"type\": [\"integer\", \"string\"],\n              \"description\": \"Maximum number of restart attempts before giving up.\"\n            },\n            \"window\": {\n              \"type\": \"string\",\n              \"description\": \"Time window used to evaluate the restart policy (e.g., '1s', '1m30s').\"\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        },\n        \"placement\": {\n          \"type\": \"object\",\n          \"description\": \"Constraints and preferences for the platform to select a physical node to run service containers\",\n          \"properties\": {\n            \"constraints\": {\n              \"type\": \"array\",\n              \"items\": {\"type\": \"string\"},\n              \"description\": \"Placement constraints for the service (e.g., 'node.role==manager').\"\n            },\n            \"preferences\": {\n              \"type\": \"array\",\n              \"description\": \"Placement preferences for the service.\",\n              \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"spread\": {\n                    \"type\": \"string\",\n                    \"description\": \"Spread tasks evenly across values of the specified node label.\"\n                  }\n                },\n                \"additionalProperties\": false,\n                \"patternProperties\": {\"^x-\": {}}\n              }\n            },\n            \"max_replicas_per_node\": {\n              \"type\": [\"integer\", \"string\"],\n              \"description\": \"Maximum number of replicas of the service.\"\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        }\n      },\n      \"additionalProperties\": false,\n      \"patternProperties\": {\"^x-\": {}}\n    },\n\n    \"generic_resources\": {\n      \"type\": \"array\",\n      \"description\": \"User-defined resources for services, allowing services to reserve specialized hardware resources.\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"discrete_resource_spec\": {\n            \"type\": \"object\",\n            \"description\": \"Specification for discrete (countable) resources.\",\n            \"properties\": {\n              \"kind\": {\n                \"type\": \"string\",\n                \"description\": \"Type of resource (e.g., 'GPU', 'FPGA', 'SSD').\"\n              },\n              \"value\": {\n                \"type\": [\"number\", \"string\"],\n                \"description\": \"Number of resources of this kind to reserve.\"\n              }\n            },\n            \"additionalProperties\": false,\n            \"patternProperties\": {\"^x-\": {}}\n          }\n        },\n        \"additionalProperties\": false,\n        \"patternProperties\": {\"^x-\": {}}\n      }\n    },\n\n    \"devices\": {\n      \"type\": \"array\",\n      \"description\": \"Device reservations for containers, allowing services to access specific hardware devices.\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"capabilities\": {\n            \"$ref\": \"#/definitions/list_of_strings\",\n            \"description\": \"List of capabilities the device needs to have (e.g., 'gpu', 'compute', 'utility').\"\n          },\n          \"count\": {\n            \"type\": [\"string\", \"integer\"],\n            \"description\": \"Number of devices of this type to reserve.\"\n          },\n          \"device_ids\": {\n            \"$ref\": \"#/definitions/list_of_strings\",\n            \"description\": \"List of specific device IDs to reserve.\"\n          },\n          \"driver\": {\n            \"type\": \"string\",\n            \"description\": \"Device driver to use (e.g., 'nvidia').\"\n          },\n          \"options\": {\n            \"$ref\": \"#/definitions/list_or_dict\",\n            \"description\": \"Driver-specific options for the device.\"\n          }\n        },\n        \"additionalProperties\": false,\n        \"patternProperties\": {\"^x-\": {}},\n        \"required\": [\n          \"capabilities\"\n        ]\n      }\n    },\n\n    \"gpus\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"enum\": [\"all\"],\n          \"description\": \"Use all available GPUs.\"\n        },\n        {\n          \"type\": \"array\",\n          \"description\": \"List of specific GPU devices to use.\",\n          \"items\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"capabilities\": {\n                \"$ref\": \"#/definitions/list_of_strings\",\n                \"description\": \"List of capabilities the GPU needs to have (e.g., 'compute', 'utility').\"\n              },\n              \"count\": {\n                \"type\": [\"string\", \"integer\"],\n                \"description\": \"Number of GPUs to use.\"\n              },\n              \"device_ids\": {\n                \"$ref\": \"#/definitions/list_of_strings\",\n                \"description\": \"List of specific GPU device IDs to use.\"\n              },\n              \"driver\": {\n                \"type\": \"string\",\n                \"description\": \"GPU driver to use (e.g., 'nvidia').\"\n              },\n              \"options\": {\n                \"$ref\": \"#/definitions/list_or_dict\",\n                \"description\": \"Driver-specific options for the GPU.\"\n              }\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        }\n      ]\n    },\n\n    \"include\": {\n      \"description\": \"Compose application or sub-projects to be included.\",\n      \"oneOf\": [\n        {\"type\": \"string\"},\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"path\": {\n              \"$ref\": \"#/definitions/string_or_list\",\n              \"description\": \"Path to the Compose application or sub-project files to include.\"\n            },\n            \"env_file\": {\n              \"$ref\": \"#/definitions/string_or_list\",\n              \"description\": \"Path to the environment files to use to define default values when interpolating variables in the Compose files being parsed.\"\n            },\n            \"project_directory\": {\n              \"type\": \"string\",\n              \"description\": \"Path to resolve relative paths set in the Compose file\"\n            }\n          },\n          \"additionalProperties\": false\n        }\n      ]\n    },\n\n    \"network\": {\n      \"type\": [\"object\", \"null\"],\n      \"description\": \"Network configuration for the Compose application.\",\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Custom name for this network.\"\n        },\n        \"driver\": {\n          \"type\": \"string\",\n          \"description\": \"Specify which driver should be used for this network. Default is 'bridge'.\"\n        },\n        \"driver_opts\": {\n          \"type\": \"object\",\n          \"description\": \"Specify driver-specific options defined as key/value pairs.\",\n          \"patternProperties\": {\n            \"^.+$\": {\"type\": [\"string\", \"number\"]}\n          }\n        },\n        \"ipam\": {\n          \"type\": \"object\",\n          \"description\": \"Custom IP Address Management configuration for this network.\",\n          \"properties\": {\n            \"driver\": {\n              \"type\": \"string\",\n              \"description\": \"Custom IPAM driver, instead of the default.\"\n            },\n            \"config\": {\n              \"type\": \"array\",\n              \"description\": \"List of IPAM configuration blocks.\",\n              \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"subnet\": {\n                    \"type\": \"string\",\n                    \"description\": \"Subnet in CIDR format that represents a network segment.\"\n                  },\n                  \"ip_range\": {\n                    \"type\": \"string\",\n                    \"description\": \"Range of IPs from which to allocate container IPs.\"\n                  },\n                  \"gateway\": {\n                    \"type\": \"string\",\n                    \"description\": \"IPv4 or IPv6 gateway for the subnet.\"\n                  },\n                  \"aux_addresses\": {\n                    \"type\": \"object\",\n                    \"description\": \"Auxiliary IPv4 or IPv6 addresses used by Network driver.\",\n                    \"additionalProperties\": false,\n                    \"patternProperties\": {\"^.+$\": {\"type\": \"string\"}}\n                  }\n                },\n                \"additionalProperties\": false,\n                \"patternProperties\": {\"^x-\": {}}\n              }\n            },\n            \"options\": {\n              \"type\": \"object\",\n              \"description\": \"Driver-specific options for the IPAM driver.\",\n              \"additionalProperties\": false,\n              \"patternProperties\": {\"^.+$\": {\"type\": \"string\"}}\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        },\n        \"external\": {\n          \"type\": [\"boolean\", \"string\", \"object\"],\n          \"description\": \"Specifies that this network already exists and was created outside of Compose.\",\n          \"properties\": {\n            \"name\": {\n              \"deprecated\": true,\n              \"type\": \"string\",\n              \"description\": \"Specifies the name of the external network. Deprecated: use the 'name' property instead.\"\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        },\n        \"internal\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Create an externally isolated network.\"\n        },\n        \"enable_ipv4\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Enable IPv4 networking.\"\n        },\n        \"enable_ipv6\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Enable IPv6 networking.\"\n        },\n        \"attachable\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"If true, standalone containers can attach to this network.\"\n        },\n        \"labels\": {\n          \"$ref\": \"#/definitions/list_or_dict\",\n          \"description\": \"Add metadata to the network using labels.\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"patternProperties\": {\"^x-\": {}}\n    },\n\n    \"volume\": {\n      \"type\": [\"object\", \"null\"],\n      \"description\": \"Volume configuration for the Compose application.\",\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Custom name for this volume.\"\n        },\n        \"driver\": {\n          \"type\": \"string\",\n          \"description\": \"Specify which volume driver should be used for this volume.\"\n        },\n        \"driver_opts\": {\n          \"type\": \"object\",\n          \"description\": \"Specify driver-specific options.\",\n          \"patternProperties\": {\n            \"^.+$\": {\"type\": [\"string\", \"number\"]}\n          }\n        },\n        \"external\": {\n          \"type\": [\"boolean\", \"string\", \"object\"],\n          \"description\": \"Specifies that this volume already exists and was created outside of Compose.\",\n          \"properties\": {\n            \"name\": {\n              \"deprecated\": true,\n              \"type\": \"string\",\n              \"description\": \"Specifies the name of the external volume. Deprecated: use the 'name' property instead.\"\n            }\n          },\n          \"additionalProperties\": false,\n          \"patternProperties\": {\"^x-\": {}}\n        },\n        \"labels\": {\n          \"$ref\": \"#/definitions/list_or_dict\",\n          \"description\": \"Add metadata to the volume using labels.\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"patternProperties\": {\"^x-\": {}}\n    },\n\n    \"secret\": {\n      \"type\": \"object\",\n      \"description\": \"Secret configuration for the Compose application.\",\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Custom name for this secret.\"\n        },\n        \"environment\": {\n          \"type\": \"string\",\n          \"description\": \"Name of an environment variable from which to get the secret value.\"\n        },\n        \"file\": {\n          \"type\": \"string\",\n          \"description\": \"Path to a file containing the secret value.\"\n        },\n        \"external\": {\n          \"type\": [\"boolean\", \"string\", \"object\"],\n          \"description\": \"Specifies that this secret already exists and was created outside of Compose.\",\n          \"properties\": {\n            \"name\": {\n              \"type\": \"string\",\n              \"description\": \"Specifies the name of the external secret.\"\n            }\n          }\n        },\n        \"labels\": {\n          \"$ref\": \"#/definitions/list_or_dict\",\n          \"description\": \"Add metadata to the secret using labels.\"\n        },\n        \"driver\": {\n          \"type\": \"string\",\n          \"description\": \"Specify which secret driver should be used for this secret.\"\n        },\n        \"driver_opts\": {\n          \"type\": \"object\",\n          \"description\": \"Specify driver-specific options.\",\n          \"patternProperties\": {\n            \"^.+$\": {\"type\": [\"string\", \"number\"]}\n          }\n        },\n        \"template_driver\": {\n          \"type\": \"string\",\n          \"description\": \"Driver to use for templating the secret's value.\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"patternProperties\": {\"^x-\": {}}\n    },\n\n    \"config\": {\n      \"type\": \"object\",\n      \"description\": \"Config configuration for the Compose application.\",\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Custom name for this config.\"\n        },\n        \"content\": {\n          \"type\": \"string\",\n          \"description\": \"Inline content of the config.\"\n        },\n        \"environment\": {\n          \"type\": \"string\",\n          \"description\": \"Name of an environment variable from which to get the config value.\"\n        },\n        \"file\": {\n          \"type\": \"string\",\n          \"description\": \"Path to a file containing the config value.\"\n        },\n        \"external\": {\n          \"type\": [\"boolean\", \"string\", \"object\"],\n          \"description\": \"Specifies that this config already exists and was created outside of Compose.\",\n          \"properties\": {\n            \"name\": {\n              \"deprecated\": true,\n              \"type\": \"string\",\n              \"description\": \"Specifies the name of the external config. Deprecated: use the 'name' property instead.\"\n            }\n          }\n        },\n        \"labels\": {\n          \"$ref\": \"#/definitions/list_or_dict\",\n          \"description\": \"Add metadata to the config using labels.\"\n        },\n        \"template_driver\": {\n          \"type\": \"string\",\n          \"description\": \"Driver to use for templating the config's value.\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"patternProperties\": {\"^x-\": {}}\n    },\n\n    \"model\": {\n      \"type\": \"object\",\n      \"description\": \"Language Model for the Compose application.\",\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Custom name for this model.\"\n        },\n        \"model\": {\n          \"type\": \"string\",\n          \"description\": \"Language Model to run.\"\n        },\n        \"context_size\": {\n          \"type\": \"integer\"\n        },\n        \"runtime_flags\": {\n          \"type\": \"array\",\n          \"items\": {\"type\": \"string\"},\n          \"description\": \"Raw runtime flags to pass to the inference engine.\"\n        }\n      },\n      \"required\": [\"model\"],\n      \"additionalProperties\": false,\n      \"patternProperties\": {\"^x-\": {}}\n    },\n\n    \"command\": {\n      \"oneOf\": [\n        {\n          \"type\": \"null\",\n          \"description\": \"No command specified, use the container's default command.\"\n        },\n        {\n          \"type\": \"string\",\n          \"description\": \"Command as a string, which will be executed in a shell (e.g., '/bin/sh -c').\"\n        },\n        {\n          \"type\": \"array\",\n          \"description\": \"Command as an array of strings, which will be executed directly without a shell.\",\n          \"items\": {\n            \"type\": \"string\",\n            \"description\": \"Part of the command (executable or argument).\"\n          }\n        }\n      ],\n      \"description\": \"Command to run in the container, which can be specified as a string (shell form) or array (exec form).\"\n    },\n\n    \"service_hook\": {\n      \"type\": \"object\",\n      \"description\": \"Configuration for service lifecycle hooks, which are commands executed at specific points in a container's lifecycle.\",\n      \"properties\": {\n        \"command\": {\n          \"$ref\": \"#/definitions/command\",\n          \"description\": \"Command to execute as part of the hook.\"\n        },\n        \"user\": {\n          \"type\": \"string\",\n          \"description\": \"User to run the command as.\"\n        },\n        \"privileged\": {\n          \"type\": [\"boolean\", \"string\"],\n          \"description\": \"Whether to run the command with extended privileges.\"\n        },\n        \"working_dir\": {\n          \"type\": \"string\",\n          \"description\": \"Working directory for the command.\"\n        },\n        \"environment\": {\n          \"$ref\": \"#/definitions/list_or_dict\",\n          \"description\": \"Environment variables for the command.\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"patternProperties\": {\"^x-\": {}},\n      \"required\": [\"command\"]\n    },\n\n    \"env_file\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"description\": \"Path to a file containing environment variables.\"\n        },\n        {\n          \"type\": \"array\",\n          \"description\": \"List of paths to files containing environment variables.\",\n          \"items\": {\n            \"oneOf\": [\n              {\n                \"type\": \"string\",\n                \"description\": \"Path to a file containing environment variables.\"\n              },\n              {\n                \"type\": \"object\",\n                \"description\": \"Detailed configuration for an environment file.\",\n                \"additionalProperties\": false,\n                \"properties\": {\n                  \"path\": {\n                    \"type\": \"string\",\n                    \"description\": \"Path to the environment file.\"\n                  },\n                  \"format\": {\n                    \"type\": \"string\",\n                    \"description\": \"Format attribute lets you to use an alternative file formats for env_file. When not set, env_file is parsed according to Compose rules.\"\n                  },\n                  \"required\": {\n                    \"type\": [\"boolean\", \"string\"],\n                    \"default\": true,\n                    \"description\": \"Whether the file is required. If true and the file doesn't exist, an error will be raised.\"\n                  }\n                },\n                \"required\": [\n                  \"path\"\n                ]\n              }\n            ]\n          }\n        }\n      ]\n    },\n\n    \"label_file\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"description\": \"Path to a file containing Docker labels.\"\n        },\n        {\n          \"type\": \"array\",\n          \"description\": \"List of paths to files containing Docker labels.\",\n          \"items\": {\n            \"type\": \"string\",\n            \"description\": \"Path to a file containing Docker labels.\"\n          }\n        }\n      ]\n    },\n\n    \"string_or_list\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"description\": \"A single string value.\"\n        },\n        {\n          \"$ref\": \"#/definitions/list_of_strings\",\n          \"description\": \"A list of string values.\"\n        }\n      ],\n      \"description\": \"Either a single string or a list of strings.\"\n    },\n\n    \"list_of_strings\": {\n      \"type\": \"array\",\n      \"description\": \"A list of unique string values.\",\n      \"items\": {\n        \"type\": \"string\",\n        \"description\": \"A string value in the list.\"\n      },\n      \"uniqueItems\": true\n    },\n\n    \"list_or_dict\": {\n      \"oneOf\": [\n        {\n          \"type\": \"object\",\n          \"description\": \"A dictionary mapping keys to values.\",\n          \"patternProperties\": {\n            \".+\": {\n              \"type\": [\"string\", \"number\", \"boolean\", \"null\"],\n              \"description\": \"Value for the key, which can be a string, number, boolean, or null.\"\n            }\n          },\n          \"additionalProperties\": false\n        },\n        {\n          \"type\": \"array\",\n          \"description\": \"A list of unique string values.\",\n          \"items\": {\n            \"type\": \"string\",\n            \"description\": \"A string value in the list.\"\n          },\n          \"uniqueItems\": true\n        }\n      ],\n      \"description\": \"Either a dictionary mapping keys to values, or a list of strings.\"\n    },\n\n    \"extra_hosts\": {\n      \"oneOf\": [\n        {\n          \"type\": \"object\",\n          \"description\": \"list mapping hostnames to IP addresses.\",\n          \"patternProperties\": {\n            \".+\": {\n              \"oneOf\": [\n                {\n                  \"type\": \"string\",\n                  \"description\": \"IP address for the hostname.\"\n                },\n                {\n                  \"type\": \"array\",\n                  \"description\": \"List of IP addresses for the hostname.\",\n                  \"items\": {\n                    \"type\": \"string\",\n                    \"description\": \"IP address for the hostname.\"\n                  },\n                  \"uniqueItems\": false\n                }\n              ]\n            }\n          },\n          \"additionalProperties\": false\n        },\n        {\n          \"type\": \"array\",\n          \"description\": \"List of host:IP mappings in the format 'hostname:IP'.\",\n          \"items\": {\n            \"type\": \"string\",\n            \"description\": \"Host:IP mapping in the format 'hostname:IP'.\"\n          },\n          \"uniqueItems\": true\n        }\n      ],\n      \"description\": \"Additional hostnames to be defined in the container's /etc/hosts file.\"\n    },\n\n    \"blkio_limit\": {\n      \"type\": \"object\",\n      \"description\": \"Block IO limit for a specific device.\",\n      \"properties\": {\n        \"path\": {\n          \"type\": \"string\",\n          \"description\": \"Path to the device (e.g., '/dev/sda').\"\n        },\n        \"rate\": {\n          \"type\": [\"integer\", \"string\"],\n          \"description\": \"Rate limit in bytes per second or IO operations per second.\"\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"blkio_weight\": {\n      \"type\": \"object\",\n      \"description\": \"Block IO weight for a specific device.\",\n      \"properties\": {\n        \"path\": {\n          \"type\": \"string\",\n          \"description\": \"Path to the device (e.g., '/dev/sda').\"\n        },\n        \"weight\": {\n          \"type\": [\"integer\", \"string\"],\n          \"description\": \"Relative weight for the device, between 10 and 1000.\"\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"service_config_or_secret\": {\n      \"type\": \"array\",\n      \"description\": \"Configuration for service configs or secrets, defining how they are mounted in the container.\",\n      \"items\": {\n        \"oneOf\": [\n          {\n            \"type\": \"string\",\n            \"description\": \"Name of the config or secret to grant access to.\"\n          },\n          {\n            \"type\": \"object\",\n            \"description\": \"Detailed configuration for a config or secret.\",\n            \"properties\": {\n              \"source\": {\n                \"type\": \"string\",\n                \"description\": \"Name of the config or secret as defined in the top-level configs or secrets section.\"\n              },\n              \"target\": {\n                \"type\": \"string\",\n                \"description\": \"Path in the container where the config or secret will be mounted. Defaults to /<source> for configs and /run/secrets/<source> for secrets.\"\n              },\n              \"uid\": {\n                \"type\": \"string\",\n                \"description\": \"UID of the file in the container. Default is 0 (root).\"\n              },\n              \"gid\": {\n                \"type\": \"string\",\n                \"description\": \"GID of the file in the container. Default is 0 (root).\"\n              },\n              \"mode\": {\n                \"type\": [\"number\", \"string\"],\n                \"description\": \"File permission mode inside the container, in octal. Default is 0444 for configs and 0400 for secrets.\"\n              }\n            },\n            \"additionalProperties\": false,\n            \"patternProperties\": {\"^x-\": {}}\n          }\n        ]\n      }\n    },\n    \"ulimits\": {\n      \"type\": \"object\",\n      \"description\": \"Container ulimit options, controlling resource limits for processes inside the container.\",\n      \"patternProperties\": {\n        \"^[a-z]+$\": {\n          \"oneOf\": [\n            {\n              \"type\": [\"integer\", \"string\"],\n              \"description\": \"Single value for both soft and hard limits.\"\n            },\n            {\n              \"type\": \"object\",\n              \"description\": \"Separate soft and hard limits.\",\n              \"properties\": {\n                \"hard\": {\n                  \"type\": [\"integer\", \"string\"],\n                  \"description\": \"Hard limit for the ulimit type. This is the maximum allowed value.\"\n                },\n                \"soft\": {\n                  \"type\": [\"integer\", \"string\"],\n                  \"description\": \"Soft limit for the ulimit type. This is the value that's actually enforced.\"\n                }\n              },\n              \"required\": [\"soft\", \"hard\"],\n              \"additionalProperties\": false,\n              \"patternProperties\": {\"^x-\": {}}\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/runfile.toml",
    "content": "[dev-frontend]\nalias = \"df\"\ndescription = \"starts the frontend in dev mode\"\ncmd = \"yarn dev\"\n\n[build-frontend]\nalias = \"bf\"\ndescription = \"generates fresh ts client and builds the frontend\"\ncmd = \"yarn build\"\nafter = \"gen-client\""
  },
  {
    "path": "frontend/src/components/alert/details.tsx",
    "content": "import { ResourceLink } from \"@components/resources/common\";\nimport { useRead } from \"@lib/hooks\";\nimport { UsableResource } from \"@types\";\nimport { Button } from \"@ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { useState } from \"react\";\nimport { AlertLevel } from \".\";\nimport { fmt_date_with_minutes } from \"@lib/formatting\";\nimport { DialogDescription } from \"@radix-ui/react-dialog\";\nimport {\n  alert_level_intention,\n  text_color_class_by_intention,\n} from \"@lib/color\";\nimport { MonacoEditor } from \"@components/monaco\";\nimport { Types } from \"komodo_client\";\n\nexport const AlertDetailsDialog = ({ id }: { id: string }) => {\n  const [open, set] = useState(false);\n  const alert = useRead(\"GetAlert\", { id }).data;\n  return (\n    <Dialog open={open} onOpenChange={set}>\n      <DialogTrigger asChild>\n        <Button variant=\"secondary\" className=\"items-center gap-2\">\n          Details\n        </Button>\n      </DialogTrigger>\n      <AlertDetailsDialogContent alert={alert} onClose={() => set(false)} />\n    </Dialog>\n  );\n};\n\nexport const AlertDetailsDialogContent = ({\n  alert,\n  onClose,\n}: {\n  alert: Types.Alert | undefined;\n  onClose: () => void;\n}) => (\n  <>\n    {alert && (\n      <DialogContent className=\"w-[90vw] max-w-[700px]\">\n        {alert && (\n          <>\n            <DialogHeader>\n              {alert && (\n                <div className=\"flex items-center gap-4\">\n                  <ResourceLink\n                    type={alert.target.type as UsableResource}\n                    id={alert.target.id}\n                    onClick={onClose}\n                  />\n                  <div className=\"text-muted-foreground\">\n                    {fmt_date_with_minutes(new Date(alert.ts))}\n                  </div>\n                </div>\n              )}\n            </DialogHeader>\n            <DialogDescription>\n              <div className=\"flex flex-col gap-4\">\n                <div className=\"flex gap-4 items-center\">\n                  {/** Alert type */}\n                  <div className=\"flex gap-2\">\n                    <div className=\"text-muted-foreground\">type:</div>{\" \"}\n                    {alert.data.type}\n                  </div>\n\n                  {/** Resolved */}\n                  <div className=\"flex gap-2\">\n                    <div className=\"text-muted-foreground\">status:</div>{\" \"}\n                    <div\n                      className={text_color_class_by_intention(\n                        alert.resolved\n                          ? \"Good\"\n                          : alert_level_intention(alert.level)\n                      )}\n                    >\n                      {alert.resolved ? \"RESOLVED\" : \"OPEN\"}\n                    </div>\n                  </div>\n\n                  {/** Level */}\n                  <div className=\"flex gap-2 text-muted-foreground\">\n                    level: <AlertLevel level={alert.level} />\n                  </div>\n                </div>\n\n                {/** Alert data */}\n                <MonacoEditor\n                  value={JSON.stringify(alert.data.data, undefined, 2)}\n                  language=\"json\"\n                  readOnly\n                />\n              </div>\n            </DialogDescription>\n          </>\n        )}\n      </DialogContent>\n    )}\n  </>\n);\n"
  },
  {
    "path": "frontend/src/components/alert/index.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { alert_level_intention } from \"@lib/color\";\nimport { useRead, useLocalStorage } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Button } from \"@ui/button\";\nimport { AlertTriangle } from \"lucide-react\";\nimport { AlertsTable } from \"./table\";\nimport { StatusBadge } from \"@components/util\";\n\nexport const OpenAlerts = () => {\n  const [open, setOpen] = useLocalStorage(\"open-alerts-v0\", true);\n  const alerts = useRead(\"ListAlerts\", { query: { resolved: false } }).data\n    ?.alerts;\n  if (!alerts || alerts.length === 0) return null;\n  return (\n    <Section\n      title=\"Open Alerts\"\n      icon={<AlertTriangle className=\"w-4 h-4\" />}\n      actions={\n        <Button variant=\"ghost\" onClick={() => setOpen(!open)}>\n          {open ? \"hide\" : \"show\"}\n        </Button>\n      }\n    >\n      {open && <AlertsTable alerts={alerts ?? []} />}\n    </Section>\n  );\n};\n\nexport const AlertLevel = ({\n  level,\n}: {\n  level: Types.SeverityLevel | undefined;\n}) => {\n  if (!level) return null;\n  return <StatusBadge text={level} intent={alert_level_intention(level)} />;\n};\n"
  },
  {
    "path": "frontend/src/components/alert/table.tsx",
    "content": "import { Types } from \"komodo_client\";\nimport { DataTable } from \"@ui/data-table\";\nimport { AlertLevel } from \".\";\nimport { AlertDetailsDialog } from \"./details\";\nimport { UsableResource } from \"@types\";\nimport { ResourceLink } from \"@components/resources/common\";\nimport {\n  alert_level_intention,\n  text_color_class_by_intention,\n} from \"@lib/color\";\n\nexport const AlertsTable = ({\n  alerts,\n  showResolved,\n}: {\n  alerts: Types.Alert[];\n  showResolved?: boolean;\n}) => {\n  return (\n    <DataTable\n      tableKey=\"alerts\"\n      data={alerts ?? []}\n      columns={[\n        {\n          header: \"Details\",\n          cell: ({ row }) =>\n            row.original._id?.$oid && (\n              <AlertDetailsDialog id={row.original._id?.$oid} />\n            ),\n        },\n        {\n          header: \"Resource\",\n          cell: ({ row }) => {\n            const type = row.original.target.type as UsableResource;\n            return <ResourceLink type={type} id={row.original.target.id} />;\n          },\n        },\n        showResolved && {\n          header: \"Status\",\n          cell: ({ row }) => {\n            return (\n              <div\n                className={text_color_class_by_intention(\n                  row.original.resolved\n                    ? \"Good\"\n                    : alert_level_intention(row.original.level)\n                )}\n              >\n                {row.original.resolved ? \"RESOLVED\" : \"OPEN\"}\n              </div>\n            );\n          },\n        },\n        {\n          header: \"Level\",\n          cell: ({ row }) => <AlertLevel level={row.original.level} />,\n        },\n        {\n          header: \"Alert Type\",\n          accessorKey: \"data.type\",\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/config/env_vars.tsx",
    "content": "import { SecretSelector } from \"@components/config/util\";\nimport { useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { useToast } from \"@ui/use-toast\";\n\nexport const SecretsSearch = ({\n  server,\n}: {\n  /// eg server id\n  server?: string;\n}) => {\n  if (server) return <SecretsWithServer server={server} />;\n  return <SecretsNoServer />;\n};\n\nconst SecretsNoServer = () => {\n  const variables = useRead(\"ListVariables\", {}).data ?? [];\n  const secrets = useRead(\"ListSecrets\", {}).data ?? [];\n  return <SecretsView variables={variables} secrets={secrets} />;\n};\n\nconst SecretsWithServer = ({\n  server,\n}: {\n  /// eg server id\n  server: string;\n}) => {\n  const variables = useRead(\"ListVariables\", {}).data ?? [];\n  const secrets =\n    useRead(\"ListSecrets\", { target: { type: \"Server\", id: server } }).data ??\n    [];\n  return <SecretsView variables={variables} secrets={secrets} />;\n};\n\nconst SecretsView = ({\n  variables,\n  secrets,\n}: {\n  variables: Types.ListVariablesResponse;\n  secrets: Types.ListSecretsResponse;\n}) => {\n  const { toast } = useToast();\n  if (variables.length === 0 && secrets.length === 0) return;\n  return (\n    <div className=\"flex items-center gap-2\">\n      {variables.length > 0 && (\n        <SecretSelector\n          type=\"Variable\"\n          keys={variables.map((v) => v.name)}\n          onSelect={(variable) => {\n            if (!variable) return;\n            navigator.clipboard.writeText(\"[[\" + variable + \"]]\");\n            toast({ title: \"Copied selection\" });\n          }}\n          disabled={false}\n          side=\"right\"\n          align=\"start\"\n        />\n      )}\n      {secrets.length > 0 && (\n        <SecretSelector\n          type=\"Secret\"\n          keys={secrets}\n          onSelect={(secret) => {\n            if (!secret) return;\n            navigator.clipboard.writeText(\"[[\" + secret + \"]]\");\n            toast({ title: \"Copied selection\" });\n          }}\n          disabled={false}\n          side=\"right\"\n          align=\"start\"\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/config/index.tsx",
    "content": "import {\n  ConfigInput,\n  ConfigSwitch,\n  ConfirmUpdate,\n} from \"@components/config/util\";\nimport { Section } from \"@components/layouts\";\nimport { MonacoLanguage } from \"@components/monaco\";\nimport { Types } from \"komodo_client\";\nimport { cn } from \"@lib/utils\";\nimport { Button } from \"@ui/button\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { AlertTriangle, History, Settings } from \"lucide-react\";\nimport { Fragment, ReactNode, SetStateAction } from \"react\";\n\nconst keys = <T extends Record<string, unknown>>(obj: T) =>\n  Object.keys(obj) as Array<keyof T>;\n\nexport const ConfigLayout = <\n  T extends Types.Resource<unknown, unknown>[\"config\"],\n>({\n  original,\n  update,\n  children,\n  disabled,\n  onConfirm,\n  onReset,\n  selector,\n  titleOther,\n  file_contents_language,\n}: {\n  original: T;\n  update: Partial<T>;\n  children: ReactNode;\n  disabled: boolean;\n  onConfirm: () => void;\n  onReset: () => void;\n  selector?: ReactNode;\n  titleOther?: ReactNode;\n  file_contents_language?: MonacoLanguage;\n}) => {\n  const titleProps = titleOther\n    ? { titleOther }\n    : { title: \"Config\", icon: <Settings className=\"w-4 h-4\" /> };\n  const changesMade = Object.keys(update).length ? true : false;\n  return (\n    <Section\n      {...titleProps}\n      actions={\n        <div className=\"flex gap-2\">\n          {changesMade && (\n            <div className=\"text-muted-foreground flex items-center gap-2\">\n              <AlertTriangle className=\"w-4 h-4\" /> Unsaved changes\n              <AlertTriangle className=\"w-4 h-4\" />\n            </div>\n          )}\n          {selector}\n          {changesMade && (\n            <>\n              <Button\n                variant=\"outline\"\n                onClick={onReset}\n                disabled={disabled || !changesMade}\n                className=\"flex items-center gap-2\"\n              >\n                <History className=\"w-4 h-4\" />\n                Reset\n              </Button>\n              <ConfirmUpdate\n                previous={original}\n                content={update}\n                onConfirm={async () => onConfirm()}\n                disabled={disabled}\n                file_contents_language={file_contents_language}\n                key_listener\n              />\n            </>\n          )}\n        </div>\n      }\n    >\n      {children}\n    </Section>\n  );\n};\n\nexport type PrimitiveConfigArgs = {\n  placeholder?: string;\n  label?: string;\n  boldLabel?: boolean;\n  description?: ReactNode;\n};\n\nexport type ConfigComponent<T> = {\n  label: string;\n  boldLabel?: boolean; // defaults to true\n  icon?: ReactNode;\n  actions?: ReactNode;\n  labelExtra?: ReactNode;\n  description?: ReactNode;\n  hidden?: boolean;\n  labelHidden?: boolean;\n  contentHidden?: boolean;\n  components: {\n    [K in keyof Partial<T>]:\n      | boolean\n      | PrimitiveConfigArgs\n      | ((value: T[K], set: (value: Partial<T>) => void) => ReactNode);\n  };\n};\n\nexport const Config = <T,>({\n  original,\n  update,\n  disabled,\n  disableSidebar,\n  set,\n  onSave,\n  components,\n  selector,\n  titleOther,\n  file_contents_language,\n}: {\n  original: T;\n  update: Partial<T>;\n  disabled: boolean;\n  disableSidebar?: boolean;\n  set: React.Dispatch<SetStateAction<Partial<T>>>;\n  onSave: () => Promise<void>;\n  selector?: ReactNode;\n  titleOther?: ReactNode;\n  components: Record<\n    string, // sidebar key\n    ConfigComponent<T>[] | false | undefined\n  >;\n  file_contents_language?: MonacoLanguage;\n}) => {\n  const sections = keys(components).filter((section) => !!components[section]);\n  const changesMade = Object.keys(update).length ? true : false;\n  const onConfirm = async () => {\n    await onSave();\n    set({});\n  };\n  const onReset = () => set({});\n  return (\n    <ConfigLayout\n      original={original}\n      titleOther={titleOther}\n      update={update}\n      disabled={disabled}\n      onConfirm={onConfirm}\n      onReset={onReset}\n      selector={selector}\n      file_contents_language={file_contents_language}\n    >\n      <div className=\"flex gap-6\">\n        {!disableSidebar && (\n          <div className=\"hidden xl:block relative pr-6 border-r\">\n            <div className=\"sticky top-24 hidden xl:flex flex-col gap-8 w-[140px] h-fit pb-24\">\n              {sections.map((section) => (\n                <div key={section}>\n                  {section && (\n                    <p className=\"text-muted-foreground uppercase text-right mb-2\">\n                      {section}\n                    </p>\n                  )}\n                  <div className=\"flex flex-col gap-2\">\n                    {components[section] &&\n                      components[section]\n                        .filter((item) => !item.hidden)\n                        .map((item) => (\n                          // uses a tags becasue react-router-dom Links don't reliably hash scroll\n                          <a\n                            href={\"#\" + section + item.label}\n                            key={section + item.label}\n                          >\n                            <Button\n                              variant=\"secondary\"\n                              className=\"justify-end w-full\"\n                              size=\"sm\"\n                            >\n                              {item.label}\n                            </Button>\n                          </a>\n                        ))}\n                  </div>\n                </div>\n              ))}\n              {changesMade && (\n                <div className=\"flex flex-col gap-2\">\n                  <ConfirmUpdate\n                    previous={original}\n                    content={update}\n                    onConfirm={onConfirm}\n                    disabled={disabled || !changesMade}\n                    file_contents_language={file_contents_language}\n                  />\n                  <Button\n                    variant=\"outline\"\n                    onClick={onReset}\n                    disabled={disabled || !changesMade}\n                    className=\"flex items-center gap-2\"\n                  >\n                    <History className=\"w-4 h-4\" />\n                    Reset\n                  </Button>\n                </div>\n              )}\n            </div>\n          </div>\n        )}\n        <div className=\"w-full flex flex-col gap-12\">\n          {sections.map(\n            (section) =>\n              components[section] && (\n                <div\n                  key={section}\n                  className=\"relative pb-12 border-b last:pb-0 last:border-b-0 \"\n                >\n                  <div className=\"xl:hidden sticky top-16 h-16 flex items-center justify-between bg-background z-10\">\n                    {section && <p className=\"uppercase text-2xl\">{section}</p>}\n                    <Select\n                      onValueChange={(value) => (window.location.hash = value)}\n                    >\n                      <SelectTrigger className=\"w-32 capitalize xl:hidden\">\n                        <SelectValue placeholder=\"Go To\" />\n                      </SelectTrigger>\n                      <SelectContent className=\"w-32\">\n                        {components[section]\n                          .filter((item) => !item.hidden)\n                          .map(({ label }) => (\n                            <SelectItem\n                              key={section + label}\n                              value={section + label}\n                              className=\"capitalize\"\n                            >\n                              {label}\n                            </SelectItem>\n                          ))}\n                      </SelectContent>\n                    </Select>\n                  </div>\n                  {section && (\n                    <p className=\"hidden xl:block bg-background text-2xl uppercase mb-6 h-fit\">\n                      {section}\n                    </p>\n                  )}\n                  <div className=\"flex flex-col gap-6 w-full\">\n                    {components[section].map(\n                      ({\n                        label,\n                        boldLabel = true,\n                        labelHidden,\n                        icon,\n                        labelExtra,\n                        actions,\n                        description,\n                        hidden,\n                        contentHidden,\n                        components,\n                      }) => (\n                        <div\n                          key={section + label}\n                          id={section + label}\n                          className={cn(\n                            \"p-6 border rounded-md flex flex-col gap-6 scroll-mt-40 xl:scroll-mt-24\",\n                            hidden && \"hidden\"\n                          )}\n                        >\n                          {!labelHidden && (\n                            <div className=\"flex justify-between\">\n                              <div>\n                                <div className=\"flex items-center gap-4\">\n                                  {icon}\n                                  <div\n                                    className={cn(\n                                      \"text-lg\",\n                                      boldLabel && \"font-bold\"\n                                    )}\n                                  >\n                                    {label}\n                                  </div>\n                                  {labelExtra}\n                                </div>\n                                {description && (\n                                  <div className=\"text-sm text-muted-foreground\">\n                                    {description}\n                                  </div>\n                                )}\n                              </div>\n                              {actions}\n                            </div>\n                          )}\n                          {!contentHidden && (\n                            <ConfigAgain\n                              config={original}\n                              update={update}\n                              set={(u) => set((p) => ({ ...p, ...u }))}\n                              components={components}\n                              disabled={disabled}\n                            />\n                          )}\n                        </div>\n                      )\n                    )}\n                  </div>\n                </div>\n              )\n          )}\n          {changesMade && (\n            <div className=\"flex gap-2 justify-end\">\n              <div className=\"text-muted-foreground flex items-center gap-2\">\n                <AlertTriangle className=\"w-4 h-4\" /> Unsaved changes\n                <AlertTriangle className=\"w-4 h-4\" />\n              </div>\n              <Button\n                variant=\"outline\"\n                onClick={onReset}\n                disabled={disabled}\n                className=\"flex items-center gap-2\"\n              >\n                <History className=\"w-4 h-4\" />\n                Reset\n              </Button>\n              <ConfirmUpdate\n                previous={original}\n                content={update}\n                onConfirm={onConfirm}\n                disabled={disabled}\n                file_contents_language={file_contents_language}\n              />\n            </div>\n          )}\n        </div>\n      </div>\n    </ConfigLayout>\n  );\n};\n\nexport const ConfigAgain = <\n  T extends Types.Resource<unknown, unknown>[\"config\"],\n>({\n  config,\n  update,\n  disabled,\n  components,\n  set,\n}: {\n  config: T;\n  update: Partial<T>;\n  disabled: boolean;\n  components: Partial<{\n    [K in keyof T extends string ? keyof T : never]:\n      | boolean\n      | PrimitiveConfigArgs\n      | ((value: T[K], set: (value: Partial<T>) => void) => ReactNode);\n  }>;\n  set: (value: Partial<T>) => void;\n}) => {\n  return (\n    <>\n      {keys(components).map((key) => {\n        const component = components[key];\n        const value = update[key] ?? config[key];\n        if (typeof component === \"function\") {\n          return (\n            <Fragment key={key.toString()}>{component(value, set)}</Fragment>\n          );\n        } else if (typeof component === \"object\" || component === true) {\n          const args =\n            typeof component === \"object\"\n              ? (component as PrimitiveConfigArgs)\n              : undefined;\n          switch (typeof value) {\n            case \"string\":\n              return (\n                <ConfigInput\n                  key={key.toString()}\n                  label={args?.label ?? key.toString()}\n                  value={value}\n                  onChange={(value) => set({ [key]: value } as Partial<T>)}\n                  disabled={disabled}\n                  placeholder={args?.placeholder}\n                  description={args?.description}\n                  boldLabel={args?.boldLabel}\n                />\n              );\n            case \"number\":\n              return (\n                <ConfigInput\n                  key={key.toString()}\n                  label={args?.label ?? key.toString()}\n                  value={Number(value)}\n                  onChange={(value) =>\n                    set({ [key]: Number(value) } as Partial<T>)\n                  }\n                  disabled={disabled}\n                  placeholder={args?.placeholder}\n                  description={args?.description}\n                  boldLabel={args?.boldLabel}\n                />\n              );\n            case \"boolean\":\n              return (\n                <ConfigSwitch\n                  key={key.toString()}\n                  label={args?.label ?? key.toString()}\n                  value={value}\n                  onChange={(value) => set({ [key]: value } as Partial<T>)}\n                  disabled={disabled}\n                  description={args?.description}\n                  boldLabel={args?.boldLabel}\n                />\n              );\n            default:\n              return (\n                <div key={key.toString()}>{args?.label ?? key.toString()}</div>\n              );\n          }\n        } else {\n          return <Fragment key={key.toString()} />;\n        }\n      })}\n    </>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/config/linked_repo.tsx",
    "content": "import { ResourceLink, ResourceSelector } from \"@components/resources/common\";\nimport { ConfigItem } from \"./util\";\n\nexport const LinkedRepoConfig = ({\n  linked_repo,\n  repo_linked,\n  set,\n  disabled,\n}: {\n  linked_repo: string | undefined;\n  repo_linked: boolean;\n  set: (update: {\n    linked_repo: string;\n    // Set other props back to default.\n    git_provider: string;\n    git_account: string;\n    git_https: boolean;\n    repo: string;\n    branch: string;\n    commit: string;\n  }) => void;\n  disabled: boolean;\n}) => {\n  return (\n    <ConfigItem\n      label={\n        linked_repo ? (\n          <div className=\"flex gap-3 text-lg font-bold\">\n            Repo:\n            <ResourceLink type=\"Repo\" id={linked_repo} />\n          </div>\n        ) : (\n          \"Select Repo\"\n        )\n      }\n      description={`Select an existing Repo to attach${!repo_linked ? \", or configure the repo below\" : \"\"}.`}\n    >\n      <ResourceSelector\n        type=\"Repo\"\n        selected={linked_repo}\n        onSelect={(linked_repo) =>\n          set({\n            linked_repo,\n            // Set other props back to default.\n            git_provider: \"github.com\",\n            git_account: \"\",\n            git_https: true,\n            repo: linked_repo ? \"\" : \"namespace/repo\",\n            branch: \"main\",\n            commit: \"\",\n          })\n        }\n        disabled={disabled}\n        align=\"start\"\n      />\n    </ConfigItem>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/config/maintenance.tsx",
    "content": "import { Button } from \"@ui/button\";\nimport { Input } from \"@ui/input\";\nimport { Switch } from \"@ui/switch\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { Badge } from \"@ui/badge\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { Types } from \"komodo_client\";\nimport { useState } from \"react\";\nimport {\n  PlusCircle,\n  Pen,\n  Trash2,\n  Clock,\n  Calendar,\n  CalendarDays,\n} from \"lucide-react\";\nimport { TimezoneSelector } from \"@components/util\";\n\nexport const MaintenanceWindows = ({\n  windows,\n  onUpdate,\n  disabled,\n}: {\n  windows: Types.MaintenanceWindow[];\n  onUpdate: (windows: Types.MaintenanceWindow[]) => void;\n  disabled: boolean;\n}) => {\n  const [isCreating, setIsCreating] = useState(false);\n  const [editingWindow, setEditingWindow] = useState<\n    [number, Types.MaintenanceWindow] | null\n  >(null);\n\n  const addWindow = (newWindow: Types.MaintenanceWindow) => {\n    onUpdate([...windows, newWindow]);\n    setIsCreating(false);\n  };\n\n  const updateWindow = (\n    index: number,\n    updatedWindow: Types.MaintenanceWindow\n  ) => {\n    onUpdate(windows.map((w, i) => (i === index ? updatedWindow : w)));\n    setEditingWindow(null);\n  };\n\n  const deleteWindow = (index: number) => {\n    onUpdate(windows.filter((_, i) => i !== index));\n  };\n\n  const toggleWindow = (index: number, enabled: boolean) => {\n    onUpdate(windows.map((w, i) => (i === index ? { ...w, enabled } : w)));\n  };\n\n  return (\n    <div className=\"space-y-4\">\n      {!disabled && (\n        <Dialog open={isCreating} onOpenChange={setIsCreating}>\n          <DialogTrigger asChild>\n            <Button variant=\"secondary\" className=\"flex items-center gap-2\">\n              <PlusCircle className=\"w-4 h-4\" />\n              Add Maintenance Window\n            </Button>\n          </DialogTrigger>\n          <DialogContent className=\"max-w-2xl\">\n            <MaintenanceWindowForm\n              onSave={addWindow}\n              onCancel={() => setIsCreating(false)}\n            />\n          </DialogContent>\n        </Dialog>\n      )}\n\n      {windows.length > 0 && (\n        <DataTable\n          tableKey=\"maintenance-windows\"\n          data={windows}\n          columns={[\n            {\n              accessorKey: \"name\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Name\" />\n              ),\n              cell: ({ row }) => (\n                <div className=\"flex items-center gap-2\">\n                  <ScheduleIcon\n                    scheduleType={\n                      row.original.schedule_type ??\n                      Types.MaintenanceScheduleType.Daily\n                    }\n                  />\n                  <span className=\"font-medium\">{row.original.name}</span>\n                </div>\n              ),\n              size: 200,\n            },\n            {\n              accessorKey: \"schedule_type\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Schedule\" />\n              ),\n              cell: ({ row }) => (\n                <span className=\"text-sm\">\n                  <ScheduleDescription window={row.original} />\n                </span>\n              ),\n              size: 150,\n            },\n            {\n              accessorKey: \"start_time\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Start Time\" />\n              ),\n              cell: ({ row }) => (\n                <span className=\"text-sm font-mono\">\n                  {formatTime(row.original)}\n                </span>\n              ),\n              size: 180,\n            },\n            {\n              accessorKey: \"duration_minutes\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Duration\" />\n              ),\n              cell: ({ row }) => (\n                <span className=\"text-sm\">\n                  {row.original.duration_minutes} min\n                </span>\n              ),\n              size: 100,\n            },\n            {\n              accessorKey: \"enabled\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Status\" />\n              ),\n              cell: ({ row }) => (\n                <div className=\"flex items-center gap-2\">\n                  <Badge\n                    variant={row.original.enabled ? \"default\" : \"secondary\"}\n                  >\n                    {row.original.enabled ? \"Enabled\" : \"Disabled\"}\n                  </Badge>\n                  {!disabled && (\n                    <Switch\n                      checked={row.original.enabled}\n                      onCheckedChange={(enabled) =>\n                        toggleWindow(row.index, enabled)\n                      }\n                    />\n                  )}\n                </div>\n              ),\n              size: 120,\n            },\n            {\n              id: \"actions\",\n              header: \"Actions\",\n              cell: ({ row }) =>\n                !disabled && (\n                  <div className=\"flex items-center gap-1\">\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      onClick={() =>\n                        setEditingWindow([row.index, row.original])\n                      }\n                      className=\"h-8 w-8 p-0\"\n                    >\n                      <Pen className=\"w-4 h-4\" />\n                    </Button>\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      onClick={() => deleteWindow(row.index)}\n                      className=\"h-8 w-8 p-0 text-destructive hover:text-destructive\"\n                    >\n                      <Trash2 className=\"w-4 h-4\" />\n                    </Button>\n                  </div>\n                ),\n              size: 100,\n            },\n          ]}\n        />\n      )}\n\n      {editingWindow && (\n        <Dialog\n          open={!!editingWindow}\n          onOpenChange={() => setEditingWindow(null)}\n        >\n          <DialogContent className=\"max-w-2xl\">\n            <MaintenanceWindowForm\n              initialData={editingWindow[1]}\n              onSave={(window) => updateWindow(editingWindow[0], window)}\n              onCancel={() => setEditingWindow(null)}\n            />\n          </DialogContent>\n        </Dialog>\n      )}\n    </div>\n  );\n};\n\nconst ScheduleIcon = ({\n  scheduleType,\n}: {\n  scheduleType: Types.MaintenanceScheduleType;\n}) => {\n  switch (scheduleType) {\n    case \"Daily\":\n      return <Clock className=\"w-4 h-4\" />;\n    case \"Weekly\":\n      return <Calendar className=\"w-4 h-4\" />;\n    case \"OneTime\":\n      return <CalendarDays className=\"w-4 h-4\" />;\n    default:\n      return <Clock className=\"w-4 h-4\" />;\n  }\n};\n\nconst ScheduleDescription = ({\n  window,\n}: {\n  window: Types.MaintenanceWindow;\n}): string => {\n  switch (window.schedule_type) {\n    case \"Daily\":\n      return \"Daily\";\n    case \"Weekly\":\n      return `Weekly (${window.day_of_week || \"Monday\"})`;\n    case \"OneTime\":\n      return `One-time (${window.date || \"No date\"})`;\n    default:\n      return \"Unknown\";\n  }\n};\n\nconst formatTime = (window: Types.MaintenanceWindow) => {\n  const hours = window.hour!.toString().padStart(2, \"0\");\n  const minutes = window.minute!.toString().padStart(2, \"0\");\n  return `${hours}:${minutes} ${window.timezone ? `(${window.timezone})` : \"\"}`;\n};\n\ninterface MaintenanceWindowFormProps {\n  initialData?: Types.MaintenanceWindow;\n  onSave: (window: Types.MaintenanceWindow) => void;\n  onCancel: () => void;\n}\n\nconst MaintenanceWindowForm = ({\n  initialData,\n  onSave,\n  onCancel,\n}: MaintenanceWindowFormProps) => {\n  const [formData, setFormData] = useState<Types.MaintenanceWindow>(\n    initialData || {\n      name: \"\",\n      description: \"\",\n      schedule_type: Types.MaintenanceScheduleType.Daily,\n      day_of_week: \"\",\n      date: \"\",\n      hour: 5,\n      minute: 0,\n      timezone: \"\",\n      duration_minutes: 60,\n      enabled: true,\n    }\n  );\n\n  const [errors, setErrors] = useState<Record<string, string>>({});\n\n  const validate = (): boolean => {\n    const newErrors: Record<string, string> = {};\n\n    if (!formData.name.trim()) {\n      newErrors.name = \"Name is required\";\n    }\n\n    if (formData.hour! < 0 || formData.hour! > 23) {\n      newErrors.hour = \"Hour must be between 0 and 23\";\n    }\n\n    if (formData.minute! < 0 || formData.minute! > 59) {\n      newErrors.minute = \"Minute must be between 0 and 59\";\n    }\n\n    if (formData.duration_minutes <= 0) {\n      newErrors.duration = \"Duration must be greater than 0\";\n    }\n\n    if (formData.schedule_type && formData.schedule_type === \"OneTime\") {\n      const date = formData.date;\n      if (!date || !/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n        newErrors.date = \"Date must be in YYYY-MM-DD format\";\n      }\n    }\n\n    setErrors(newErrors);\n    return Object.keys(newErrors).length === 0;\n  };\n\n  const handleSave = () => {\n    if (validate()) {\n      onSave(formData);\n    }\n  };\n\n  const updateScheduleType = (schedule_type: Types.MaintenanceScheduleType) => {\n    setFormData((data) => ({\n      ...data,\n      schedule_type,\n      day_of_week:\n        schedule_type === Types.MaintenanceScheduleType.Weekly ? \"Monday\" : \"\",\n      date:\n        schedule_type === Types.MaintenanceScheduleType.OneTime\n          ? new Date().toISOString().split(\"T\")[0]\n          : \"\",\n    }));\n  };\n\n  return (\n    <>\n      <DialogHeader>\n        <DialogTitle>\n          {initialData\n            ? \"Edit Maintenance Window\"\n            : \"Create Maintenance Window\"}\n        </DialogTitle>\n      </DialogHeader>\n\n      <div className=\"space-y-4\">\n        <div>\n          <label className=\"text-sm font-medium\">Name</label>\n          <Input\n            value={formData.name}\n            onChange={(e) =>\n              setFormData((data) => ({ ...data, name: e.target.value }))\n            }\n            placeholder=\"e.g., Daily Backup\"\n            className={errors.name ? \"border-destructive\" : \"\"}\n          />\n          {errors.name && (\n            <p className=\"text-sm text-destructive mt-1\">{errors.name}</p>\n          )}\n        </div>\n\n        <div>\n          <label className=\"text-sm font-medium\">Schedule Type</label>\n          <Select\n            value={formData.schedule_type}\n            onValueChange={(value: Types.MaintenanceScheduleType) =>\n              updateScheduleType(value)\n            }\n          >\n            <SelectTrigger>\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              {Object.values(Types.MaintenanceScheduleType).map(\n                (schedule_type) => (\n                  <SelectItem key={schedule_type} value={schedule_type}>\n                    {schedule_type}\n                  </SelectItem>\n                )\n              )}\n            </SelectContent>\n          </Select>\n        </div>\n\n        {formData.schedule_type === \"Weekly\" && (\n          <div>\n            <label className=\"text-sm font-medium\">Day of Week</label>\n            <Select\n              value={formData.day_of_week || \"Monday\"}\n              onValueChange={(value: Types.DayOfWeek) =>\n                setFormData((data) => ({\n                  ...data,\n                  day_of_week: value,\n                }))\n              }\n            >\n              <SelectTrigger>\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                {Object.values(Types.DayOfWeek).map((day_of_week) => (\n                  <SelectItem key={day_of_week} value={day_of_week}>\n                    {day_of_week}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n        )}\n\n        {formData.schedule_type === \"OneTime\" && (\n          <div>\n            <label className=\"text-sm font-medium\">Date</label>\n            <Input\n              type=\"date\"\n              value={formData.date || new Date().toISOString().split(\"T\")[0]}\n              onChange={(e) =>\n                setFormData({\n                  ...formData,\n                  date: e.target.value,\n                })\n              }\n              className={errors.date ? \"border-destructive\" : \"\"}\n            />\n            {errors.date && (\n              <p className=\"text-sm text-destructive mt-1\">{errors.date}</p>\n            )}\n          </div>\n        )}\n\n        <div className=\"grid grid-cols-2 gap-4\">\n          <div>\n            <label className=\"text-sm font-medium\">Start Time</label>\n            <Input\n              type=\"time\"\n              value={`${formData.hour!.toString().padStart(2, \"0\")}:${formData.minute!.toString().padStart(2, \"0\")}`}\n              onChange={(e) => {\n                const [hour, minute] = e.target.value\n                  .split(\":\")\n                  .map((n) => parseInt(n) || 0);\n                setFormData({\n                  ...formData,\n                  hour,\n                  minute,\n                });\n              }}\n              className={\n                errors.hour || errors.minute ? \"border-destructive\" : \"\"\n              }\n            />\n            {(errors.hour || errors.minute) && (\n              <p className=\"text-sm text-destructive mt-1\">\n                {errors.hour || errors.minute}\n              </p>\n            )}\n          </div>\n          <div>\n            <label className=\"text-sm font-medium\">Timezone</label>\n            <TimezoneSelector\n              timezone={formData.timezone ?? \"\"}\n              onChange={(timezone) =>\n                setFormData((data) => ({ ...data, timezone }))\n              }\n              triggerClassName=\"w-full\"\n            />\n          </div>\n        </div>\n\n        <div>\n          <label className=\"text-sm font-medium\">Duration (minutes)</label>\n          <Input\n            type=\"number\"\n            min={1}\n            value={formData.duration_minutes}\n            onChange={(e) =>\n              setFormData((data) => ({\n                ...data,\n                duration_minutes: parseInt(e.target.value) || 60,\n              }))\n            }\n            className={errors.duration ? \"border-destructive\" : \"\"}\n          />\n          {errors.duration && (\n            <p className=\"text-sm text-destructive mt-1\">{errors.duration}</p>\n          )}\n        </div>\n\n        <div>\n          <label className=\"text-sm font-medium\">Description (optional)</label>\n          <Input\n            value={formData.description}\n            onChange={(e) =>\n              setFormData((data) => ({ ...data, description: e.target.value }))\n            }\n            placeholder=\"e.g., Automated backup process\"\n          />\n        </div>\n      </div>\n\n      <DialogFooter>\n        <Button variant=\"outline\" onClick={onCancel}>\n          Cancel\n        </Button>\n        <Button onClick={handleSave}>\n          {initialData ? \"Update\" : \"Create\"}\n        </Button>\n      </DialogFooter>\n    </>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/config/util.tsx",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n  WebhookIdOrName,\n  useCtrlKeyListener,\n  useRead,\n  useWebhookIdOrName,\n  WebhookIntegration,\n  useWebhookIntegrations,\n  useSettingsView,\n  usePromptHotkeys,\n} from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport {\n  Select,\n  SelectTrigger,\n  SelectValue,\n  SelectContent,\n  SelectItem,\n} from \"@ui/select\";\nimport { Button } from \"@ui/button\";\nimport { Input } from \"@ui/input\";\nimport { Switch } from \"@ui/switch\";\nimport {\n  CheckCircle,\n  MinusCircle,\n  PlusCircle,\n  Save,\n  Search,\n  SearchX,\n  SquareArrowOutUpRight,\n} from \"lucide-react\";\nimport { ReactNode, useState } from \"react\";\nimport { cn, env_to_text, filterBySplit } from \"@lib/utils\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { snake_case_to_upper_space_case } from \"@lib/formatting\";\nimport { ConfirmButton, ShowHideButton } from \"@components/util\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\n\nimport { Card, CardContent, CardHeader } from \"@ui/card\";\nimport {\n  soft_text_color_class_by_intention,\n  text_color_class_by_intention,\n} from \"@lib/color\";\nimport {\n  MonacoDiffEditor,\n  MonacoEditor,\n  MonacoLanguage,\n} from \"@components/monaco\";\nimport { useNavigate } from \"react-router-dom\";\nimport { Badge } from \"@ui/badge\";\n\nexport const ConfigItem = ({\n  label,\n  boldLabel,\n  description,\n  children,\n  className,\n}: {\n  label?: ReactNode;\n  boldLabel?: boolean;\n  description?: ReactNode;\n  children: ReactNode;\n  className?: string;\n}) => (\n  <div\n    className={cn(\n      \"pb-6 border-b flex flex-col gap-4 first:pt-0 last:border-b-0 last:pb-0\",\n      className\n    )}\n  >\n    {(label || description) && (\n      <div>\n        {label && typeof label === \"string\" && (\n          <div className={cn(\"capitalize\", boldLabel && \"font-bold\")}>\n            {label.split(\"_\").join(\" \")}\n          </div>\n        )}\n        {label && typeof label !== \"string\" && label}\n        {description && (\n          <div className=\"text-sm text-muted-foreground\">{description}</div>\n        )}\n      </div>\n    )}\n    {children}\n  </div>\n);\n\nexport const ConfigInput = ({\n  label,\n  boldLabel,\n  value,\n  description,\n  disabled,\n  placeholder,\n  onChange,\n  onBlur,\n  className,\n  inputLeft,\n  inputRight,\n}: {\n  label: string;\n  boldLabel?: boolean;\n  value: string | number | undefined;\n  description?: ReactNode;\n  disabled?: boolean;\n  placeholder?: string;\n  onChange?: (value: string) => void;\n  onBlur?: (value: string) => void;\n  className?: string;\n  inputLeft?: ReactNode;\n  inputRight?: ReactNode;\n}) => (\n  <ConfigItem label={label} boldLabel={boldLabel} description={description}>\n    {inputLeft || inputRight ? (\n      <div className=\"flex gap-2 items-center\">\n        {inputLeft}\n        <Input\n          className={cn(\"max-w-[75%] lg:max-w-[400px]\", className)}\n          type={typeof value === \"number\" ? \"number\" : undefined}\n          value={value}\n          onChange={(e) => onChange && onChange(e.target.value)}\n          onBlur={(e) => onBlur && onBlur(e.target.value)}\n          placeholder={placeholder}\n          disabled={disabled}\n        />\n        {inputRight}\n      </div>\n    ) : (\n      <Input\n        className={cn(\"max-w-[75%] lg:max-w-[400px]\", className)}\n        type={typeof value === \"number\" ? \"number\" : undefined}\n        value={value}\n        onChange={(e) => onChange && onChange(e.target.value)}\n        onBlur={(e) => onBlur && onBlur(e.target.value)}\n        placeholder={placeholder}\n        disabled={disabled}\n      />\n    )}\n  </ConfigItem>\n);\n\nexport const ConfigSwitch = ({\n  label,\n  boldLabel,\n  value: checked,\n  description,\n  disabled,\n  onChange,\n}: {\n  label: string;\n  boldLabel?: boolean;\n  value: boolean | undefined;\n  description?: ReactNode;\n  disabled: boolean;\n  onChange: (value: boolean) => void;\n}) => (\n  <ConfigItem\n    label={label}\n    description={description}\n    boldLabel={boldLabel}\n    className=\"flex-col\"\n  >\n    <div\n      className=\"py-2 flex flex-row gap-4 items-center text-sm cursor-pointer\"\n      onClick={() => !disabled && onChange(!checked)}\n    >\n      {/* <div\n        className={cn(\n          \"transition-colors text-muted-foreground\",\n          !checked && soft_text_color_class_by_intention(\"Critical\")\n          // checked && \"text-muted-foreground\"\n        )}\n      >\n        DISABLED\n      </div> */}\n      <Switch checked={checked} disabled={disabled} />\n      <div\n        className={cn(\n          \"transition-colors\",\n          soft_text_color_class_by_intention(checked ? \"Good\" : \"Critical\")\n          // !checked && \"text-muted-foreground\"\n        )}\n      >\n        {checked ? \"ENABLED\" : \"DISABLED\"}\n      </div>\n    </div>\n  </ConfigItem>\n);\n\nexport const ConfigList = <T extends { [key: string]: unknown }>(\n  props: InputListProps<T> & {\n    label?: string;\n    addLabel?: string;\n    boldLabel?: boolean;\n    description?: ReactNode;\n    configClassname?: string;\n  }\n) => {\n  return (\n    <ConfigItem {...{ ...props, className: props.configClassname }}>\n      {!props.disabled && (\n        <Button\n          variant=\"secondary\"\n          onClick={() =>\n            props.set({\n              [props.field]: [...props.values, \"\"],\n            } as Partial<T>)\n          }\n          className=\"flex items-center gap-2 w-[200px]\"\n        >\n          <PlusCircle className=\"w-4 h-4\" />\n          {props.addLabel ??\n            (\"Add \" + props.label?.endsWith(\"s\")\n              ? props.label?.slice(0, -1)\n              : props.label)}\n        </Button>\n      )}\n      {props.values.length > 0 && <InputList {...props} />}\n    </ConfigItem>\n  );\n};\n\nexport type InputListProps<T extends { [key: string]: unknown }> = {\n  field: keyof T;\n  values: string[];\n  disabled: boolean;\n  set: (update: Partial<T>) => void;\n  placeholder?: string;\n  className?: string;\n};\n\nexport const InputList = <T extends { [key: string]: unknown }>({\n  field,\n  values,\n  disabled,\n  set,\n  placeholder,\n  className,\n}: InputListProps<T>) => (\n  <div className=\"flex w-full\">\n    <div className=\"flex flex-col gap-4 w-fit\">\n      {values.map((arg, i) => (\n        <div className=\"w-full flex gap-4\" key={i}>\n          <Input\n            placeholder={placeholder}\n            value={arg}\n            onChange={(e) => {\n              values[i] = e.target.value;\n              set({ [field]: [...values] } as Partial<T>);\n            }}\n            disabled={disabled}\n            className={cn(\"w-[400px] max-w-full\", className)}\n          />\n          {!disabled && (\n            <Button\n              variant=\"secondary\"\n              onClick={() =>\n                set({\n                  [field]: [...values.filter((_, idx) => idx !== i)],\n                } as Partial<T>)\n              }\n            >\n              <MinusCircle className=\"w-4 h-4\" />\n            </Button>\n          )}\n        </div>\n      ))}\n    </div>\n  </div>\n);\n\ninterface ConfirmUpdateProps<T> {\n  previous: T;\n  content: Partial<T>;\n  onConfirm: () => Promise<void>;\n  loading?: boolean;\n  disabled: boolean;\n  language?: MonacoLanguage;\n  file_contents_language?: MonacoLanguage;\n  key_listener?: boolean;\n}\n\nexport function ConfirmUpdate<T>({\n  previous,\n  content,\n  onConfirm,\n  loading,\n  disabled,\n  language,\n  file_contents_language,\n  key_listener = false,\n}: ConfirmUpdateProps<T>) {\n  const [open, set] = useState(false);\n\n  const handleConfirm = async () => {\n    await onConfirm();\n    set(false);\n  };\n\n  const handleCancel = () => {\n    set(false);\n  };\n\n  // Keep the existing Ctrl+Enter behavior for backward compatibility\n  useCtrlKeyListener(\"Enter\", () => {\n    if (!key_listener) return;\n    if (open) {\n      handleConfirm();\n    } else {\n      set(true);\n    }\n  });\n\n  // Add new prompt hotkeys for better UX\n  usePromptHotkeys({\n    onConfirm: handleConfirm,\n    onCancel: handleCancel,\n    enabled: open,\n    confirmDisabled: disabled || loading,\n  });\n\n  return (\n    <Dialog open={open} onOpenChange={set}>\n      <DialogTrigger asChild>\n        <Button\n          onClick={() => set(true)}\n          disabled={disabled}\n          className=\"flex items-center gap-2 w-100\"\n        >\n          <Save className=\"w-4 h-4\" />\n          Save\n        </Button>\n      </DialogTrigger>\n      <DialogContent className=\"max-w-[800px]\">\n        <DialogHeader>\n          <DialogTitle>Confirm Update</DialogTitle>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-4 py-4 my-4 max-h-[70vh] overflow-auto\">\n          {Object.entries(content).map(([key, val], i) => (\n            <ConfirmUpdateItem\n              key={i}\n              _key={key as any}\n              val={val as any}\n              previous={previous}\n              language={language}\n              file_contents_language={file_contents_language}\n            />\n          ))}\n        </div>\n        <DialogFooter>\n          <ConfirmButton\n            title=\"Update\"\n            icon={<CheckCircle className=\"w-4 h-4\" />}\n            onClick={handleConfirm}\n            loading={loading}\n          />\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\nfunction ConfirmUpdateItem<T>({\n  _key,\n  val: _val,\n  previous,\n  language,\n  file_contents_language,\n}: {\n  _key: keyof T;\n  val: T[keyof T];\n  previous: T;\n  language?: MonacoLanguage;\n  file_contents_language?: MonacoLanguage;\n}) {\n  const [show, setShow] = useState(true);\n  const val =\n    typeof _val === \"string\"\n      ? _val\n      : Array.isArray(_val)\n        ? _val.length > 0 &&\n          [\"string\", \"number\", \"boolean\"].includes(typeof _val[0])\n          ? JSON.stringify(_val)\n          : JSON.stringify(_val, null, 2)\n        : JSON.stringify(_val, null, 2);\n  const prev_val =\n    typeof previous[_key] === \"string\"\n      ? previous[_key]\n      : _key === \"environment\" ||\n          _key === \"build_args\" ||\n          _key === \"secret_args\"\n        ? (env_to_text(previous[_key] as any) ?? \"\") // For backward compat with 1.14\n        : Array.isArray(previous[_key])\n          ? previous[_key].length > 0 &&\n            [\"string\", \"number\", \"boolean\"].includes(typeof previous[_key][0])\n            ? JSON.stringify(previous[_key])\n            : JSON.stringify(previous[_key], null, 2)\n          : JSON.stringify(previous[_key], null, 2);\n  const showDiff =\n    val?.includes(\"\\n\") ||\n    prev_val?.includes(\"\\n\") ||\n    Math.max(val?.length ?? 0, prev_val?.length ?? 0) > 30;\n  return (\n    <div\n      className={cn(\"mr-6 flex flex-col gap-2\", val === prev_val && \"hidden\")}\n    >\n      <Card>\n        <CardHeader className=\"p-4 flex flex-row justify-between items-center\">\n          <h1 className={text_color_class_by_intention(\"Neutral\")}>\n            {snake_case_to_upper_space_case(_key as string)}\n          </h1>\n          <ShowHideButton show={show} setShow={setShow} />\n        </CardHeader>\n        {show && (\n          <CardContent>\n            {showDiff ? (\n              <MonacoDiffEditor\n                original={prev_val}\n                modified={val}\n                language={\n                  language ??\n                  ([\"environment\", \"build_args\", \"secret_args\"].includes(\n                    _key as string\n                  )\n                    ? \"key_value\"\n                    : _key === \"file_contents\"\n                      ? file_contents_language\n                      : \"json\")\n                }\n              />\n            ) : (\n              <pre style={{ minHeight: 0 }}>\n                <span className={text_color_class_by_intention(\"Critical\")}>\n                  {prev_val || \"None\"}\n                </span>{\" \"}\n                <span className=\"text-muted-foreground\">{\"->\"}</span>{\" \"}\n                <span className={text_color_class_by_intention(\"Good\")}>\n                  {val || \"None\"}\n                </span>\n              </pre>\n            )}\n          </CardContent>\n        )}\n      </Card>\n    </div>\n  );\n}\n\nexport const SystemCommand = ({\n  value,\n  disabled,\n  set,\n}: {\n  value?: Types.SystemCommand;\n  disabled: boolean;\n  set: (value: Types.SystemCommand) => void;\n}) => {\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <div className=\"flex items-center gap-2\">\n        <div className=\"text-muted-foreground\">Path:</div>\n        <Input\n          placeholder=\"Command working directory\"\n          value={value?.path}\n          className=\"w-[200px] lg:w-[300px]\"\n          onChange={(e) => set({ ...(value || {}), path: e.target.value })}\n          disabled={disabled}\n        />\n      </div>\n      <MonacoEditor\n        value={\n          value?.command ||\n          \"  # Add multiple commands on new lines. Supports comments.\\n  \"\n        }\n        language=\"shell\"\n        onValueChange={(command) => set({ ...(value || {}), command })}\n        readOnly={disabled}\n      />\n    </div>\n  );\n};\n\nexport const AddExtraArgMenu = ({\n  onSelect,\n  type,\n  disabled,\n}: {\n  onSelect: (suggestion: string) => void;\n  type: \"Deployment\" | \"Build\" | \"Stack\" | \"StackBuild\";\n  disabled?: boolean;\n}) => {\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const suggestions = useRead(`ListCommon${type}ExtraArgs`, {}).data ?? [];\n\n  const filtered = filterBySplit(suggestions, search, (item) => item);\n\n  if (suggestions.length === 0) {\n    return (\n      <Button\n        variant=\"secondary\"\n        className=\"flex items-center gap-2 w-[200px]\"\n        onClick={() => onSelect(\"\")}\n        disabled={disabled}\n      >\n        <PlusCircle className=\"w-4 h-4\" /> Add Extra Arg\n      </Button>\n    );\n  }\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"secondary\"\n          className=\"flex items-center gap-2 w-[200px]\"\n          disabled={disabled}\n        >\n          <PlusCircle className=\"w-4 h-4\" /> Add Extra Arg\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[300px] max-h-[400px] p-0\" align=\"start\">\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Search suggestions\"\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center\">\n              No Suggestions Found\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n\n            <CommandGroup>\n              <CommandItem\n                onSelect={() => {\n                  onSelect(\"\");\n                  setOpen(false);\n                }}\n                className=\"w-full cursor-pointer\"\n              >\n                Empty Extra Arg\n              </CommandItem>\n\n              {filtered?.map((suggestion) => (\n                <CommandItem\n                  key={suggestion}\n                  onSelect={() => {\n                    onSelect(suggestion);\n                    setOpen(false);\n                  }}\n                  className=\"w-full overflow-hidden overflow-ellipsis cursor-pointer\"\n                >\n                  {suggestion}\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nexport const ImageRegistryConfig = ({\n  registry,\n  setRegistry,\n  disabled,\n  builder_id,\n  onRemove,\n  imageName,\n}: {\n  registry: Types.ImageRegistryConfig | undefined;\n  setRegistry: (registry: Types.ImageRegistryConfig) => void;\n  disabled: boolean;\n  builder_id: string | undefined;\n  onRemove: () => void;\n  imageName: string | undefined;\n}) => {\n  // This is the only way to get organizations for now\n  const config_provider = useRead(\"ListDockerRegistriesFromConfig\", {\n    target: builder_id ? { type: \"Builder\", id: builder_id } : undefined,\n  }).data?.find((provider) => {\n    return provider.domain === registry?.domain;\n  });\n\n  const organizations = config_provider?.organizations ?? [];\n  const namespace = registry?.organization || registry?.account;\n\n  return (\n    <ConfigItem\n      label={\n        <div className=\"flex gap-2 items-center flex-wrap\">\n          <div className=\"text-muted-foreground\">Pushes to:</div>{\" \"}\n          {registry?.domain && registry.domain + \" / \"}\n          {registry?.domain && (namespace ? namespace : \"<namespace>\") + \" / \"}\n          {imageName}\n          {!registry?.domain && <Badge variant=\"secondary\">Local</Badge>}\n        </div>\n      }\n    >\n      <div className=\"flex items-center gap-4 flex-wrap\">\n        <ProviderSelector\n          disabled={disabled}\n          account_type=\"docker\"\n          selected={registry?.domain}\n          onSelect={(domain) =>\n            setRegistry({\n              ...registry,\n              domain,\n            })\n          }\n          showCustom={false}\n          showLabel\n        />\n        <AccountSelector\n          id={builder_id}\n          type=\"Builder\"\n          account_type=\"docker\"\n          provider={registry?.domain!}\n          selected={registry?.account}\n          onSelect={(account) =>\n            setRegistry({\n              ...registry,\n              account,\n            })\n          }\n          disabled={!registry?.domain || disabled}\n          showLabel\n        />\n        <OrganizationSelector\n          organizations={organizations}\n          selected={registry?.organization!}\n          set={(organization) =>\n            setRegistry({\n              ...registry,\n              organization,\n            })\n          }\n          disabled={disabled}\n          showLabel\n        />\n        {!disabled && (\n          <Button\n            variant=\"destructive\"\n            onClick={onRemove}\n            className=\"px-3 py-1\"\n          >\n            <MinusCircle className=\"w-4 h-4\" />\n          </Button>\n        )}\n      </div>\n    </ConfigItem>\n  );\n};\n\nexport const ProviderSelector = ({\n  disabled,\n  account_type,\n  selected,\n  onSelect,\n  showCustom = true,\n  showLabel,\n}: {\n  disabled: boolean;\n  account_type: \"git\" | \"docker\";\n  selected: string | undefined;\n  onSelect: (provider: string) => void;\n  showCustom?: boolean;\n  showLabel?: boolean;\n}) => {\n  const [db_request, config_request]:\n    | [\"ListGitProviderAccounts\", \"ListGitProvidersFromConfig\"]\n    | [\"ListDockerRegistryAccounts\", \"ListDockerRegistriesFromConfig\"] =\n    account_type === \"git\"\n      ? [\"ListGitProviderAccounts\", \"ListGitProvidersFromConfig\"]\n      : [\"ListDockerRegistryAccounts\", \"ListDockerRegistriesFromConfig\"];\n  const db_providers = useRead(db_request, {}).data;\n  const config_providers = useRead(config_request, {}).data;\n  const [customMode, setCustomMode] = useState(false);\n\n  if (customMode) {\n    return (\n      <Input\n        placeholder=\"Input custom provider domain\"\n        value={selected}\n        onChange={(e) => onSelect(e.target.value)}\n        className=\"max-w-[75%] lg:max-w-[400px]\"\n        onBlur={() => setCustomMode(false)}\n        onKeyDown={(e) => {\n          if (e.key === \"Enter\") {\n            setCustomMode(false);\n          }\n        }}\n        autoFocus\n      />\n    );\n  }\n\n  const domains = new Set<string>();\n  for (const provider of db_providers ?? []) {\n    domains.add(provider.domain);\n  }\n  for (const provider of config_providers ?? []) {\n    domains.add(provider.domain);\n  }\n  const providers = [...domains];\n  providers.sort();\n\n  return (\n    <Select\n      value={selected}\n      onValueChange={(value) => {\n        if (value === \"Custom\") {\n          onSelect(\"\");\n          setCustomMode(true);\n        } else if (value === \"None\") {\n          onSelect(\"\");\n        } else {\n          onSelect(value);\n        }\n      }}\n      disabled={disabled}\n    >\n      <SelectTrigger\n        className=\"w-full lg:w-[300px] md:max-w-[50%]\"\n        disabled={disabled}\n      >\n        <div className=\"flex items-center gap-2\">\n          {showLabel && selected && (\n            <div className=\"text-xs text-muted-foreground\">Domain:</div>\n          )}\n          <SelectValue placeholder=\"Select Provider\" />\n        </div>\n      </SelectTrigger>\n      <SelectContent>\n        {providers\n          ?.filter((provider) => provider)\n          .map((provider) => (\n            <SelectItem key={provider} value={provider}>\n              {provider}\n            </SelectItem>\n          ))}\n        {providers !== undefined &&\n          selected &&\n          !providers.includes(selected) && (\n            <SelectItem value={selected}>{selected}</SelectItem>\n          )}\n        {showCustom && <SelectItem value=\"Custom\">Custom</SelectItem>}\n        {!showCustom && <SelectItem value=\"None\">None</SelectItem>}\n      </SelectContent>\n    </Select>\n  );\n};\n\nexport const ProviderSelectorConfig = (params: {\n  disabled: boolean;\n  account_type: \"git\" | \"docker\";\n  selected: string | undefined;\n  onSelect: (id: string) => void;\n  https?: boolean;\n  onHttpsSwitch?: () => void;\n  description?: string;\n  boldLabel?: boolean;\n}) => {\n  const select =\n    params.account_type === \"git\" ? \"git provider\" : \"docker registry\";\n  const label =\n    params.account_type === \"git\" ? \"Git Provider\" : \"Image Registry\";\n  return (\n    <ConfigItem\n      label={label}\n      description={params.description ?? `Select ${select} domain`}\n      boldLabel={params.boldLabel}\n    >\n      {params.account_type === \"git\" ? (\n        <div className=\"flex items-center gap-2 w-[75%]\">\n          <Button\n            variant=\"outline\"\n            onClick={params.onHttpsSwitch}\n            className=\"py-0 px-2\"\n            disabled={params.disabled}\n          >\n            {`http${params.https ? \"s\" : \"\"}://`}\n          </Button>\n          <ProviderSelector {...params} />\n        </div>\n      ) : (\n        <ProviderSelector {...params} />\n      )}\n    </ConfigItem>\n  );\n};\n\nexport const AccountSelector = ({\n  disabled,\n  id,\n  type,\n  account_type,\n  provider,\n  selected,\n  onSelect,\n  placeholder = \"Select Account\",\n  showLabel,\n}: {\n  disabled: boolean;\n  type: \"Server\" | \"Builder\" | \"None\";\n  id?: string;\n  account_type: \"git\" | \"docker\";\n  provider: string;\n  selected: string | undefined;\n  onSelect: (id: string) => void;\n  placeholder?: string;\n  showLabel?: boolean;\n}) => {\n  const [db_request, config_request]:\n    | [\"ListGitProviderAccounts\", \"ListGitProvidersFromConfig\"]\n    | [\"ListDockerRegistryAccounts\", \"ListDockerRegistriesFromConfig\"] =\n    account_type === \"git\"\n      ? [\"ListGitProviderAccounts\", \"ListGitProvidersFromConfig\"]\n      : [\"ListDockerRegistryAccounts\", \"ListDockerRegistriesFromConfig\"];\n  const config_params =\n    type === \"None\" ? {} : { target: id ? { type, id } : undefined };\n  const db_accounts = useRead(db_request, {}).data?.filter(\n    (account) => account.domain === provider\n  );\n  const config_providers = useRead(config_request, config_params).data?.filter(\n    (_provider) => _provider.domain === provider\n  );\n\n  const _accounts = new Set<string>();\n  for (const account of db_accounts ?? []) {\n    if (account.username) {\n      _accounts.add(account.username);\n    }\n  }\n  for (const provider of config_providers ?? []) {\n    for (const account of provider.accounts ?? []) {\n      _accounts.add(account.username);\n    }\n  }\n  const accounts = [..._accounts];\n  accounts.sort();\n  return (\n    <Select\n      value={selected}\n      onValueChange={(value) => {\n        onSelect(value === \"Empty\" ? \"\" : value);\n      }}\n      disabled={disabled}\n    >\n      <SelectTrigger\n        className=\"w-full lg:w-[300px] md:max-w-[50%]\"\n        disabled={disabled}\n      >\n        <div className=\"flex gap-2 items-center\">\n          {showLabel && selected && (\n            <div className=\"text-xs text-muted-foreground\">Account:</div>\n          )}\n          <SelectValue placeholder={placeholder} />\n        </div>\n      </SelectTrigger>\n      <SelectContent>\n        <SelectItem value={\"Empty\"}>None</SelectItem>\n        {accounts\n          ?.filter((account) => account)\n          .map((account) => (\n            <SelectItem key={account} value={account}>\n              {account}\n            </SelectItem>\n          ))}\n      </SelectContent>\n    </Select>\n  );\n};\n\nexport const AccountSelectorConfig = (params: {\n  disabled: boolean;\n  id?: string;\n  type: \"Server\" | \"Builder\" | \"None\";\n  account_type: \"git\" | \"docker\";\n  provider: string;\n  selected: string | undefined;\n  onSelect: (id: string) => void;\n  placeholder?: string;\n  description?: string;\n}) => {\n  return (\n    <ConfigItem\n      label=\"Account\"\n      description={\n        params.description ??\n        \"Select the account used to log in to the provider\"\n      }\n    >\n      <AccountSelector {...params} />\n    </ConfigItem>\n  );\n};\n\nconst OrganizationSelector = ({\n  organizations,\n  selected,\n  set,\n  disabled,\n  showLabel,\n}: {\n  organizations: string[];\n  selected: string;\n  set: (org: string) => void;\n  disabled: boolean;\n  showLabel?: boolean;\n}) => {\n  const [customMode, setCustomMode] = useState(false);\n  if (customMode) {\n    return (\n      <Input\n        placeholder=\"Input custom organization name\"\n        value={selected}\n        onChange={(e) => set(e.target.value)}\n        className=\"max-w-[75%] lg:max-w-[400px]\"\n        onBlur={() => setCustomMode(false)}\n        onKeyDown={(e) => {\n          if (e.key === \"Enter\") {\n            setCustomMode(false);\n          }\n        }}\n        autoFocus\n      />\n    );\n  }\n\n  const orgs =\n    selected === \"\" || organizations.includes(selected)\n      ? organizations\n      : [...organizations, selected];\n  orgs.sort();\n\n  return (\n    <Select\n      value={selected}\n      onValueChange={(organization) => {\n        if (organization === \"Custom\") {\n          set(\"\");\n          setCustomMode(true);\n        } else if (organization === \"Empty\") {\n          set(\"\");\n        } else {\n          set(organization);\n        }\n      }}\n      disabled={disabled}\n    >\n      <SelectTrigger\n        className=\"w-full lg:w-[300px] md:max-w-[50%]\"\n        disabled={disabled}\n      >\n        <div className=\"flex gap-2 items-center\">\n          {showLabel && selected && (\n            <div className=\"text-xs text-muted-foreground\">Org:</div>\n          )}\n          <SelectValue placeholder=\"Select Organization\" />\n        </div>\n      </SelectTrigger>\n      <SelectContent>\n        <SelectItem value={\"Empty\"}>None</SelectItem>\n        {orgs\n          ?.filter((org) => org)\n          .map((org) => (\n            <SelectItem key={org} value={org}>\n              {org}\n            </SelectItem>\n          ))}\n        <SelectItem value={\"Custom\"}>Custom</SelectItem>\n      </SelectContent>\n    </Select>\n  );\n};\n\nexport const SecretSelector = ({\n  keys,\n  onSelect,\n  type,\n  disabled,\n  align = \"start\",\n  side = \"right\",\n}: {\n  keys: string[];\n  onSelect: (key: string) => void;\n  type: \"Variable\" | \"Secret\";\n  disabled: boolean;\n  align?: \"start\" | \"center\" | \"end\";\n  side?: \"bottom\" | \"right\";\n}) => {\n  const nav = useNavigate();\n  const [_, setSettingsView] = useSettingsView();\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const filtered = filterBySplit(keys, search, (item) => item).sort((a, b) => {\n    if (a > b) {\n      return 1;\n    } else if (a < b) {\n      return -1;\n    } else {\n      return 0;\n    }\n  });\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button variant=\"secondary\" className=\"flex gap-2\" disabled={disabled}>\n          <Search className=\"w-4 h-4\" />\n          <div>{type}s</div>\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        className=\"w-[300px] max-h-[300px] p-0\"\n        align={align}\n        side={side}\n      >\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder={`Search ${type}s`}\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center pt-2\">\n              {`No ${type}s Found`}\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n\n            <CommandGroup>\n              {filtered.map((key) => (\n                <CommandItem\n                  key={key}\n                  onSelect={() => {\n                    onSelect(key);\n                    setOpen(false);\n                  }}\n                  className=\"flex items-center justify-between cursor-pointer\"\n                >\n                  <div className=\"p-1\">{key}</div>\n                </CommandItem>\n              ))}\n              <CommandItem\n                onSelect={() => {\n                  setOpen(false);\n                  setSettingsView(\"Variables\");\n                  nav(\"/settings\");\n                }}\n                className=\"flex items-center justify-between cursor-pointer\"\n              >\n                <div className=\"p-1\">All</div>\n                <SquareArrowOutUpRight className=\"w-4 h-4\" />\n              </CommandItem>\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nexport const WebhookBuilder = ({\n  git_provider,\n  children,\n}: {\n  git_provider: string;\n  children?: ReactNode;\n}) => {\n  return (\n    <ConfigItem>\n      <div className=\"grid items-center grid-cols-[auto_1fr] gap-x-6 gap-y-2 w-fit\">\n        <div className=\"text-muted-foreground text-sm\">Auth style?</div>\n        <WebhookIntegrationSelector git_provider={git_provider} />\n\n        <div className=\"text-muted-foreground text-sm\">\n          Resource Id or Name?\n        </div>\n        <WebhookIdOrNameSelector />\n\n        {children}\n      </div>\n    </ConfigItem>\n  );\n};\n\n/** Should call `useWebhookIntegrations` in util/hooks to get the current value */\nexport const WebhookIntegrationSelector = ({\n  git_provider,\n}: {\n  git_provider: string;\n}) => {\n  const { integrations, setIntegration } = useWebhookIntegrations();\n  const integration = integrations[git_provider]\n    ? integrations[git_provider]\n    : git_provider === \"gitlab.com\"\n      ? \"Gitlab\"\n      : \"Github\";\n  return (\n    <Select\n      value={integration}\n      onValueChange={(integration) =>\n        setIntegration(git_provider, integration as WebhookIntegration)\n      }\n    >\n      <SelectTrigger className=\"w-[200px]\">\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {[\"Github\", \"Gitlab\"].map((integration) => (\n          <SelectItem key={integration} value={integration}>\n            {integration}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n};\n\n/** Should call `useWebhookIdOrName` in util/hooks to get the current value */\nexport const WebhookIdOrNameSelector = () => {\n  const [idOrName, setIdOrName] = useWebhookIdOrName();\n  return (\n    <Select\n      value={idOrName}\n      onValueChange={(idOrName) => setIdOrName(idOrName as WebhookIdOrName)}\n    >\n      <SelectTrigger className=\"w-[200px]\">\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {[\"Id\", \"Name\"].map((idOrName) => (\n          <SelectItem key={idOrName} value={idOrName}>\n            {idOrName}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/export.tsx",
    "content": "import { useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Button } from \"@ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { FileDown, Loader2 } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { CopyButton } from \"./util\";\nimport { MonacoEditor } from \"./monaco\";\n\nexport const ExportButton = ({\n  targets,\n  user_groups,\n  tags,\n  include_variables,\n}: {\n  targets?: Types.ResourceTarget[];\n  user_groups?: string[];\n  tags?: string[];\n  include_variables?: boolean;\n}) => {\n  const [open, setOpen] = useState(false);\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button variant=\"secondary\" className=\"flex gap-2 items-center\">\n          <FileDown className=\"w-4 h-4\" />\n          Toml\n        </Button>\n      </DialogTrigger>\n      <DialogContent className=\"w-[900px] max-w-[95vw]\">\n        <DialogHeader>\n          <DialogTitle>Export to Toml</DialogTitle>\n        </DialogHeader>\n        {targets || user_groups || include_variables ? (\n          <ExportTargetsLoader\n            targets={targets}\n            user_groups={user_groups}\n            include_variables={include_variables}\n          />\n        ) : (\n          <ExportAllLoader tags={tags} />\n        )}\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst ExportTargetsLoader = ({\n  targets,\n  user_groups,\n  include_variables,\n}: {\n  targets?: Types.ResourceTarget[];\n  user_groups?: string[];\n  include_variables?: boolean;\n}) => {\n  const { data, isPending } = useRead(\"ExportResourcesToToml\", {\n    targets: targets ? targets : [],\n    user_groups: user_groups ? user_groups : [],\n    include_variables,\n  });\n  return <ExportPre loading={isPending} content={data?.toml} />;\n};\n\nconst ExportAllLoader = ({\n  tags,\n}: {\n  tags?: string[];\n}) => {\n  const { data, isPending } = useRead(\"ExportAllResourcesToToml\", {\n    tags,\n    include_resources: true,\n    include_variables: true,\n    include_user_groups: true,\n  });\n  return <ExportPre loading={isPending} content={data?.toml} />;\n};\n\nconst ExportPre = ({\n  loading,\n  content,\n}: {\n  loading: boolean;\n  content: string | undefined;\n}) => {\n  return (\n    <div className=\"relative flex justify-center w-full overflow-y-scroll max-h-[80vh]\">\n      {loading && <Loader2 className=\"w-8 h-8 animate-spin\" />}\n      <MonacoEditor value={content} language=\"fancy_toml\" readOnly />\n      <CopyButton content={content} className=\"absolute top-4 right-4\" />\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/group-actions.tsx",
    "content": "import { useSelectedResources, useExecute, useWrite } from \"@lib/hooks\";\nimport { UsableResource } from \"@types\";\nimport { Button } from \"@ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@ui/dialog\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@ui/dropdown-menu\";\nimport { Input } from \"@ui/input\";\nimport { Types } from \"komodo_client\";\nimport { ChevronDown, CheckCircle } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { ConfirmButton } from \"./util\";\nimport { useToast } from \"@ui/use-toast\";\nimport { usableResourceExecuteKey } from \"@lib/utils\";\n\nexport const GroupActions = <\n  T extends Types.ExecuteRequest[\"type\"] | Types.WriteRequest[\"type\"],\n>({\n  type,\n  actions,\n}: {\n  type: UsableResource;\n  actions: T[];\n}) => {\n  const [action, setAction] = useState<T>();\n  const [selected] = useSelectedResources(type);\n\n  return (\n    <>\n      <GroupActionDropdownMenu\n        type={type}\n        actions={actions}\n        onSelect={setAction}\n        disabled={!selected.length}\n      />\n      <GroupActionDialog\n        type={type}\n        action={action}\n        onClose={() => setAction(undefined)}\n      />\n    </>\n  );\n};\n\nconst GroupActionDropdownMenu = <\n  T extends Types.ExecuteRequest[\"type\"] | Types.WriteRequest[\"type\"],\n>({\n  type,\n  actions,\n  onSelect,\n  disabled,\n}: {\n  type: UsableResource;\n  actions: T[];\n  onSelect: (item: T) => void;\n  disabled: boolean;\n}) => (\n  <DropdownMenu>\n    <DropdownMenuTrigger asChild disabled={disabled}>\n      <Button variant=\"outline\" className=\"w-40 justify-between\">\n        Group Actions <ChevronDown className=\"w-4\" />\n      </Button>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent\n      align=\"start\"\n      className={type === \"Server\" ? \"w-56\" : \"w-40\"}\n    >\n      {type === \"ResourceSync\" && (\n        <DropdownMenuItem\n          onClick={() => onSelect(\"RefreshResourceSyncPending\" as any)}\n        >\n          <Button variant=\"secondary\" className=\"w-full\">\n            Refresh\n          </Button>\n        </DropdownMenuItem>\n      )}\n      {actions.map((action) => (\n        <DropdownMenuItem key={action} onClick={() => onSelect(action)}>\n          <Button variant=\"secondary\" className=\"w-full\">\n            {action === \"RunBuild\"\n              ? \"Build\"\n              : action.replaceAll(\"Batch\", \"\").replaceAll(type, \"\")}\n          </Button>\n        </DropdownMenuItem>\n      ))}\n      <DropdownMenuItem onClick={() => onSelect(`Delete${type}` as any)}>\n        <Button variant=\"destructive\" className=\"w-full\">\n          Delete\n        </Button>\n      </DropdownMenuItem>\n    </DropdownMenuContent>\n  </DropdownMenu>\n);\n\nconst GroupActionDialog = ({\n  type,\n  action,\n  onClose,\n}: {\n  type: UsableResource;\n  action:\n    | (Types.ExecuteRequest[\"type\"] | Types.WriteRequest[\"type\"])\n    | undefined;\n  onClose: () => void;\n}) => {\n  const { toast } = useToast();\n  const [selected, setSelected] = useSelectedResources(type);\n  const [text, setText] = useState(\"\");\n\n  const { mutate: execute, isPending: executePending } = useExecute(\n    action! as Types.ExecuteRequest[\"type\"],\n    {\n      onSuccess: onClose,\n    }\n  );\n  const { mutate: write, isPending: writePending } = useWrite(\n    action! as Types.WriteRequest[\"type\"],\n    {\n      onSuccess: onClose,\n    }\n  );\n\n  if (!action) return;\n\n  const formatted = action.replaceAll(\"Batch\", \"\").replaceAll(type, \"\");\n  const isPending = executePending || writePending;\n\n  return (\n    <Dialog open={!!action} onOpenChange={(o) => !o && onClose()}>\n      <DialogContent className=\"max-h-[85vh] overflow-y-auto\">\n        <DialogHeader>\n          <DialogTitle>Group Execute - {formatted}</DialogTitle>\n        </DialogHeader>\n        <div className=\"py-8 flex flex-col gap-4\">\n          <ul className=\"p-4 bg-accent text-sm list-disc list-inside max-h-[300px] overflow-y-auto\">\n            {selected.map((resource) => (\n              <li key={resource}>{resource}</li>\n            ))}\n          </ul>\n          {!action.startsWith(\"Refresh\") && (\n            <>\n              <p\n                onClick={() => {\n                  navigator.clipboard.writeText(formatted);\n                  toast({ title: `Copied \"${formatted}\" to clipboard!` });\n                }}\n                className=\"cursor-pointer\"\n              >\n                Please enter <b>{formatted}</b> below to confirm this action.\n                <br />\n                <span className=\"text-xs text-muted-foreground\">\n                  You may click the action in bold to copy it\n                </span>\n              </p>\n              <Input value={text} onChange={(e) => setText(e.target.value)} />\n            </>\n          )}\n        </div>\n        <DialogFooter>\n          <ConfirmButton\n            title=\"Confirm\"\n            icon={<CheckCircle className=\"w-4 h-4\" />}\n            onClick={() => {\n              for (const resource of selected) {\n                if (action.startsWith(\"Delete\")) {\n                  write({ id: resource } as any);\n                } else if (action.startsWith(\"Refresh\")) {\n                  write({ [usableResourceExecuteKey(type)]: resource } as any);\n                } else {\n                  execute({\n                    [usableResourceExecuteKey(type)]: resource,\n                  } as any);\n                }\n              }\n              if (action.startsWith(\"Delete\")) {\n                setSelected([]);\n              }\n            }}\n            disabled={action.startsWith(\"Refresh\") ? false : text !== formatted}\n            loading={isPending}\n          />\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/inspect.tsx",
    "content": "import { Types } from \"komodo_client\";\nimport { Loader2 } from \"lucide-react\";\nimport { MonacoEditor } from \"./monaco\";\n\nexport const InspectContainerView = ({\n  container,\n  error,\n  isPending,\n  isError,\n}: {\n  container: Types.Container | undefined;\n  error: unknown;\n  isPending: boolean;\n  isError: boolean;\n}) => {\n  if (isPending) {\n    return (\n      <div className=\"flex justify-center w-full py-4 min-h-[60vh]\">\n        <Loader2 className=\"w-8 h-8 animate-spin\" />\n      </div>\n    );\n  }\n  if (isError) {\n    return (\n      <div className=\"min-h-[60vh] flex flex-col\">\n        <h1 className=\"flex w-full py-4\">Failed to inspect container.</h1>\n        {(error ?? undefined) && (\n          <MonacoEditor\n            value={JSON.stringify(error, null, 2)}\n            language=\"json\"\n            readOnly\n          />\n        )}\n      </div>\n    );\n  }\n  return (\n    <div className=\"min-h-[60vh]\">\n      <MonacoEditor\n        value={JSON.stringify(container, null, 2)}\n        language=\"json\"\n        readOnly\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/keys/table.tsx",
    "content": "import { CopyButton } from \"@components/util\";\nimport { Types } from \"komodo_client\";\nimport { DataTable } from \"@ui/data-table\";\nimport { Input } from \"@ui/input\";\nimport { ReactNode } from \"react\";\n\nconst ONE_DAY_MS = 1000 * 60 * 60 * 24;\n\nexport const KeysTable = ({\n  keys,\n  DeleteKey,\n}: {\n  keys: Types.ApiKey[];\n  DeleteKey: (params: { api_key: string }) => ReactNode;\n}) => {\n  return (\n    <DataTable\n      tableKey=\"api-keys\"\n      data={keys}\n      columns={[\n        { header: \"Name\", accessorKey: \"name\" },\n        {\n          header: \"Key\",\n          cell: ({\n            row: {\n              original: { key },\n            },\n          }) => {\n            return (\n              <div className=\"flex items-center gap-2\">\n                <Input\n                  className=\"w-[100px] lg:w-[200px] overflow-ellipsis\"\n                  value={key}\n                  disabled\n                />\n                <CopyButton content={key} />\n              </div>\n            );\n          },\n        },\n        {\n          header: \"Expires\",\n          accessorFn: ({ expires }) =>\n            expires\n              ? \"In \" +\n                ((expires - Date.now()) / ONE_DAY_MS).toFixed() +\n                \" Days\"\n              : \"Never\",\n        },\n        {\n          header: \"Delete\",\n          cell: ({ row }) => <DeleteKey api_key={row.original.key} />,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/layouts.tsx",
    "content": "import { Button } from \"@ui/button\";\nimport { PlusCircle } from \"lucide-react\";\nimport { ReactNode, useState } from \"react\";\nimport { Link, Outlet, useNavigate } from \"react-router-dom\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { Types } from \"komodo_client\";\nimport { ResourceComponents } from \"./resources\";\nimport { Card, CardHeader, CardTitle, CardContent, CardFooter } from \"@ui/card\";\nimport { ResourceTags } from \"./tags\";\nimport { Topbar } from \"./topbar\";\nimport { cn, usableResourcePath } from \"@lib/utils\";\nimport { Sidebar } from \"./sidebar\";\nimport { ResourceNameSimple } from \"./resources/common\";\nimport { useShiftKeyListener } from \"@lib/hooks\";\n\nexport const Layout = () => {\n  const nav = useNavigate();\n  useShiftKeyListener(\"H\", () => nav(\"/\"));\n  useShiftKeyListener(\"G\", () => nav(\"/servers\"));\n  useShiftKeyListener(\"Z\", () => nav(\"/stacks\"));\n  useShiftKeyListener(\"D\", () => nav(\"/deployments\"));\n  useShiftKeyListener(\"B\", () => nav(\"/builds\"));\n  useShiftKeyListener(\"R\", () => nav(\"/repos\"));\n  useShiftKeyListener(\"P\", () => nav(\"/procedures\"));\n\n  return (\n    <>\n      <Topbar />\n      <div className=\"h-screen overflow-y-scroll\">\n        <div className=\"container\">\n          <Sidebar />\n          <div className=\"lg:ml-[200px] lg:pl-8 py-[88px]\">\n            <Outlet />\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n\ninterface PageProps {\n  title?: ReactNode;\n  icon?: ReactNode;\n  titleRight?: ReactNode;\n  titleOther?: ReactNode;\n  children?: ReactNode;\n  subtitle?: ReactNode;\n  actions?: ReactNode;\n  superHeader?: ReactNode;\n}\n\nexport const Page = ({\n  superHeader,\n  title,\n  icon,\n  titleRight,\n  titleOther,\n  subtitle,\n  actions,\n  children,\n}: PageProps) => (\n  <div className=\"w-full flex flex-col gap-12\">\n    {superHeader ? (\n      <div className=\"flex flex-col gap-4\">\n        {superHeader}\n        {(title || icon || subtitle || actions) && (\n          <div\n            className={`flex flex-col gap-6 md:flex-row md:gap-0 md:justify-between`}\n          >\n            <div className=\"flex flex-col gap-4\">\n              <div className=\"flex flex-wrap gap-4 items-center\">\n                {icon}\n                <h1 className=\"text-4xl\">{title}</h1>\n                {titleRight}\n              </div>\n              <div className=\"flex flex-col\">{subtitle}</div>\n            </div>\n            {actions}\n          </div>\n        )}\n      </div>\n    ) : (\n      (title || icon || subtitle || actions) && (\n        <div\n          className={`flex flex-col gap-6 md:flex-row md:gap-0 md:justify-between`}\n        >\n          <div>\n            <div className=\"flex flex-wrap gap-4 items-center\">\n              {icon}\n              <h1 className=\"text-4xl\">{title}</h1>\n              {titleRight}\n            </div>\n            <div className=\"flex flex-col\">{subtitle}</div>\n          </div>\n          {actions}\n        </div>\n      )\n    )}\n    {titleOther}\n    {children}\n  </div>\n);\n\nexport const PageXlRow = ({\n  superHeader,\n  title,\n  icon,\n  titleRight,\n  titleOther,\n  subtitle,\n  actions,\n  children,\n}: PageProps) => (\n  <div className=\"flex flex-col gap-10 container py-8 pr-12\">\n    {superHeader ? (\n      <div className=\"flex flex-col gap-4\">\n        {superHeader}\n        {(title || icon || subtitle || actions) && (\n          <div\n            className={`flex flex-col gap-6 lg:flex-row lg:gap-0 lg:justify-between`}\n          >\n            <div className=\"flex flex-col gap-4\">\n              <div className=\"flex flex-wrap gap-4 items-center\">\n                {icon}\n                <h1 className=\"text-4xl\">{title}</h1>\n                {titleRight}\n              </div>\n              <div className=\"flex flex-col\">{subtitle}</div>\n            </div>\n            {actions}\n          </div>\n        )}\n      </div>\n    ) : (\n      (title || icon || subtitle || actions) && (\n        <div\n          className={`flex flex-col gap-6 lg:flex-row lg:gap-0 lg:justify-between`}\n        >\n          <div className=\"flex flex-col gap-4\">\n            <div className=\"flex flex-wrap gap-4 items-center\">\n              {icon}\n              <h1 className=\"text-4xl\">{title}</h1>\n              {titleRight}\n            </div>\n            <div className=\"flex flex-col\">{subtitle}</div>\n          </div>\n          {actions}\n        </div>\n      )\n    )}\n    {titleOther}\n    {children}\n  </div>\n);\n\ninterface SectionProps {\n  title?: ReactNode;\n  icon?: ReactNode;\n  titleRight?: ReactNode;\n  titleOther?: ReactNode;\n  children?: ReactNode;\n  actions?: ReactNode;\n  // otherwise items-start\n  itemsCenterTitleRow?: boolean;\n  className?: string;\n}\n\nexport const Section = ({\n  title,\n  icon,\n  titleRight,\n  titleOther,\n  actions,\n  children,\n  itemsCenterTitleRow,\n  className,\n}: SectionProps) => (\n  <div className={cn(\"flex flex-col gap-4\", className)}>\n    {(title || icon || titleRight || titleOther || actions) && (\n      <div\n        className={cn(\n          \"flex flex-wrap gap-2 justify-between\",\n          itemsCenterTitleRow ? \"items-center\" : \"items-start\"\n        )}\n      >\n        {title || icon ? (\n          <div className=\"px-2 flex items-center gap-2 text-muted-foreground\">\n            {icon}\n            {title && <h2 className=\"text-xl\">{title}</h2>}\n            {titleRight}\n          </div>\n        ) : (\n          titleOther\n        )}\n        {actions}\n      </div>\n    )}\n    {children}\n  </div>\n);\n\nexport const NewLayout = ({\n  entityType,\n  children,\n  enabled,\n  onConfirm,\n  onOpenChange,\n  configureLabel = \"a unique name\",\n}: {\n  entityType: string;\n  children: ReactNode;\n  enabled: boolean;\n  onConfirm: () => Promise<unknown>;\n  onOpenChange?: (open: boolean) => void;\n  configureLabel?: string;\n}) => {\n  const [open, set] = useState(false);\n  const [loading, setLoading] = useState(false);\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(open) => {\n        set(open);\n        onOpenChange && onOpenChange(open);\n      }}\n    >\n      <DialogTrigger asChild>\n        <Button className=\"items-center gap-2\" variant=\"secondary\">\n          New {entityType} <PlusCircle className=\"w-4 h-4\" />\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>New {entityType}</DialogTitle>\n          <DialogDescription>\n            Enter {configureLabel} for the new {entityType}.\n          </DialogDescription>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-6 py-8\">{children}</div>\n        <DialogFooter>\n          <Button\n            variant=\"secondary\"\n            onClick={async () => {\n              setLoading(true);\n              try {\n                await onConfirm();\n                set(false);\n              } catch (error: any) {\n                const status = error?.status || error?.response?.status;\n                if (status !== 409 && status !== 400) {\n                  set(false);\n                }\n              } finally {\n                setLoading(false);\n              }\n            }}\n            disabled={!enabled || loading}\n          >\n            Create\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const ResourceCard = ({\n  target: { type, id },\n}: {\n  target: Exclude<Types.ResourceTarget, { type: \"System\" }>;\n}) => {\n  const Components = ResourceComponents[type];\n\n  return (\n    <Link\n      to={`/${usableResourcePath(type)}/${id}`}\n      className=\"group hover:translate-y-[-2.5%] focus:translate-y-[-2.5%] transition-transform\"\n    >\n      <Card className=\"h-full hover:bg-accent/50 group-focus:bg-accent/50 transition-colors\">\n        <CardHeader className=\"flex-row justify-between\">\n          <div>\n            <CardTitle>\n              <ResourceNameSimple type={type} id={id} />\n            </CardTitle>\n            {/* <CardDescription>\n              <Components.Description id={id} />\n            </CardDescription> */}\n          </div>\n          <Components.Icon id={id} />\n        </CardHeader>\n        <CardContent className=\"text-sm text-muted-foreground\">\n          {Object.entries(Components.Info).map(([key, Info]) => (\n            <Info key={key} id={id} />\n          ))}\n        </CardContent>\n        <CardFooter className=\"flex items-center gap-2\">\n          <ResourceTags target={{ type, id }} />\n        </CardFooter>\n      </Card>\n    </Link>\n  );\n};\n\nexport const ResourceRow = ({\n  target: { type, id },\n}: {\n  target: Exclude<Types.ResourceTarget, { type: \"System\" }>;\n}) => {\n  const Components = ResourceComponents[type];\n\n  return (\n    <Link\n      to={`/${usableResourcePath(type)}/${id}`}\n      className=\"group hover:translate-y-[-2.5%] focus:translate-y-[-2.5%] transition-transform\"\n    >\n      <Card className=\"h-full hover:bg-accent/50 group-focus:bg-accent/50 transition-colors\">\n        <CardHeader className=\"grid grid-cols-4 items-center\">\n          <CardTitle>\n            <ResourceNameSimple type={type} id={id} />\n          </CardTitle>\n          {Object.entries(Components.Info).map(([key, Info]) => (\n            <Info key={key} id={id} />\n          ))}\n          <div className=\"flex items-center gap-2\">\n            <Components.Icon id={id} />\n            {/* <CardDescription>\n              <Components.Description id={id} />\n            </CardDescription> */}\n          </div>\n        </CardHeader>\n      </Card>\n    </Link>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/log.tsx",
    "content": "import { logToHtml } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { Button } from \"@ui/button\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport {\n  AlertOctagon,\n  ChevronDown,\n  RefreshCw,\n  ScrollText,\n  X,\n} from \"lucide-react\";\nimport { ReactNode, useEffect, useRef, useState } from \"react\";\nimport { Section } from \"./layouts\";\nimport { Switch } from \"@ui/switch\";\nimport { Input } from \"@ui/input\";\nimport { ToggleGroup, ToggleGroupItem } from \"@ui/toggle-group\";\nimport { useToast } from \"@ui/use-toast\";\nimport { useLocalStorage } from \"@lib/hooks\";\n\nexport type LogStream = \"stdout\" | \"stderr\";\n\nexport const LogSection = ({\n  regular_logs,\n  search_logs,\n  titleOther,\n  extraParams,\n}: {\n  regular_logs: (\n    timestamps: boolean,\n    stream: LogStream,\n    tail: number,\n    poll: boolean\n  ) => {\n    Log: ReactNode;\n    refetch: () => void;\n    stderr: boolean;\n  };\n  search_logs: (\n    timestamps: boolean,\n    terms: string[],\n    invert: boolean,\n    poll: boolean\n  ) => { Log: ReactNode; refetch: () => void; stderr: boolean };\n  titleOther?: ReactNode;\n  extraParams?: ReactNode;\n}) => {\n  const { toast } = useToast();\n  const [timestamps, setTimestamps] = useLocalStorage(\n    \"log-timestamps-v1\",\n    false\n  );\n  const [stream, setStream] = useState<LogStream>(\"stdout\");\n  const [tail, set] = useState(\"100\");\n  const [terms, setTerms] = useState<string[]>([]);\n  const [invert, setInvert] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const [poll, setPoll] = useLocalStorage(\"log-poll-v1\", false);\n\n  const addTerm = () => {\n    if (!search.length) return;\n    if (terms.includes(search)) {\n      toast({ title: \"Search term is already present\" });\n      setSearch(\"\");\n      return;\n    }\n    setTerms([...terms, search]);\n    setSearch(\"\");\n  };\n\n  const clearSearch = () => {\n    setSearch(\"\");\n    setTerms([]);\n  };\n\n  const { Log, refetch, stderr } = terms.length\n    ? search_logs(timestamps, terms, invert, poll)\n    : regular_logs(timestamps, stream, Number(tail), poll);\n\n  return (\n    <Section\n      title={titleOther ? undefined : \"Log\"}\n      icon={titleOther ? undefined : <ScrollText className=\"w-4 h-4\" />}\n      titleOther={titleOther}\n      itemsCenterTitleRow\n      actions={\n        <div className=\"flex items-center gap-4 flex-wrap\">\n          <div className=\"flex items-center gap-2\">\n            <div className=\"text-muted-foreground flex gap-1 text-sm\">\n              Invert\n            </div>\n            <Switch checked={invert} onCheckedChange={setInvert} />\n          </div>\n          {terms.map((term, index) => (\n            <Button\n              key={term}\n              variant=\"destructive\"\n              onClick={() => setTerms(terms.filter((_, i) => i !== index))}\n              className=\"flex gap-2 items-center py-0 px-2\"\n            >\n              {term}\n              <X className=\"w-4 h-h\" />\n            </Button>\n          ))}\n          <div className=\"relative\">\n            <Input\n              placeholder=\"Search Logs\"\n              value={search}\n              onChange={(e) => setSearch(e.target.value)}\n              onBlur={addTerm}\n              onKeyDown={(e) => {\n                if (e.key === \"Enter\") addTerm();\n              }}\n              className=\"w-[180px] xl:w-[240px]\"\n            />\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={clearSearch}\n              className=\"absolute right-0 top-1/2 -translate-y-1/2\"\n            >\n              <X className=\"w-4 h-4\" />\n            </Button>\n          </div>\n          <ToggleGroup\n            type=\"single\"\n            value={stream}\n            onValueChange={setStream as any}\n          >\n            <ToggleGroupItem value=\"stdout\">stdout</ToggleGroupItem>\n            <ToggleGroupItem value=\"stderr\">\n              stderr\n              {stderr && (\n                <AlertOctagon className=\"w-4 h-4 ml-2 stroke-red-500\" />\n              )}\n            </ToggleGroupItem>\n          </ToggleGroup>\n          <Button variant=\"secondary\" size=\"icon\" onClick={() => refetch()}>\n            <RefreshCw className=\"w-4 h-4\" />\n          </Button>\n          <div\n            className=\"flex items-center gap-2 cursor-pointer\"\n            onClick={() => setTimestamps((t) => !t)}\n          >\n            <div className=\"text-muted-foreground text-sm\">Timestamps</div>\n            <Switch checked={timestamps} />\n          </div>\n          <div\n            className=\"flex items-center gap-2 cursor-pointer\"\n            onClick={() => setPoll((p) => !p)}\n          >\n            <div className=\"text-muted-foreground text-sm\">Poll</div>\n            <Switch checked={poll} />\n          </div>\n          <TailLengthSelector\n            selected={tail}\n            onSelect={set}\n            disabled={search.length > 0}\n          />\n          {extraParams}\n        </div>\n      }\n    >\n      {Log}\n    </Section>\n  );\n};\n\nexport const Log = ({\n  log,\n  stream,\n}: {\n  log: Types.Log | undefined;\n  stream: \"stdout\" | \"stderr\";\n}) => {\n  const _log = log?.[stream as keyof typeof log] as string | undefined;\n  const ref = useRef<HTMLDivElement>(null);\n  const scroll = () =>\n    ref.current?.scroll({\n      top: ref.current.scrollHeight,\n      behavior: \"smooth\",\n    });\n  useEffect(scroll, [_log]);\n  return (\n    <>\n      <div ref={ref} className=\"h-[75vh] overflow-y-auto\">\n        <pre\n          dangerouslySetInnerHTML={{\n            __html: _log ? logToHtml(_log) : `no ${stream} logs`,\n          }}\n          className=\"-scroll-mt-24 pb-[20vh]\"\n        />\n      </div>\n      <Button\n        variant=\"secondary\"\n        className=\"absolute top-4 right-4\"\n        onClick={scroll}\n      >\n        <ChevronDown className=\"h-4 w-4\" />\n      </Button>\n    </>\n  );\n};\n\nexport const TailLengthSelector = ({\n  selected,\n  onSelect,\n  disabled,\n}: {\n  selected: string;\n  onSelect: (value: string) => void;\n  disabled?: boolean;\n}) => (\n  <Select value={selected} onValueChange={onSelect} disabled={disabled}>\n    <SelectTrigger className=\"w-[120px]\">\n      <SelectValue />\n    </SelectTrigger>\n    <SelectContent>\n      <SelectGroup>\n        {[\"100\", \"500\", \"1000\", \"5000\"].map((length) => (\n          <SelectItem key={length} value={length}>\n            {length} lines\n          </SelectItem>\n        ))}\n      </SelectGroup>\n    </SelectContent>\n  </Select>\n);\n"
  },
  {
    "path": "frontend/src/components/monaco.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { DiffEditor, Editor } from \"@monaco-editor/react\";\nimport { useTheme } from \"@ui/theme\";\nimport { cn } from \"@lib/utils\";\nimport * as monaco from \"monaco-editor\";\nimport * as prettier from \"prettier/standalone\";\nimport * as pluginTypescript from \"prettier/plugins/typescript\";\nimport * as pluginEsTree from \"prettier/plugins/estree\";\nimport * as pluginYaml from \"prettier/plugins/yaml\";\nimport { useRead, useWindowDimensions } from \"@lib/hooks\";\n\nconst MIN_EDITOR_HEIGHT = 56;\n\nexport type MonacoLanguage =\n  | \"yaml\"\n  | \"toml\"\n  | \"fancy_toml\"\n  | \"json\"\n  | \"key_value\"\n  | \"ini\"\n  | \"string_list\"\n  | \"shell\"\n  | \"dockerfile\"\n  | \"rust\"\n  | \"javascript\"\n  | \"typescript\";\n\nconst LANGUAGE_EXTENSIONS: Record<MonacoLanguage, string[]> = {\n  yaml: [\".yaml\", \".yml\"],\n  toml: [\".toml\"],\n  fancy_toml: [],\n  json: [\".json\"],\n  key_value: [\".env\", \".conf\"],\n  ini: [\".ini\"],\n  string_list: [],\n  shell: [\".sh\", \".bash\", \".zsh\"],\n  dockerfile: [\"Dockerfile\"],\n  rust: [\".rs\"],\n  javascript: [\".js\", \".jsx\", \".mjs\", \".cjs\"],\n  typescript: [\".ts\", \".tsx\"],\n};\n\nexport const language_from_path = (path: string) => {\n  for (const [lang, extensions] of Object.entries(LANGUAGE_EXTENSIONS)) {\n    for (const extension of extensions) {\n      if (path.endsWith(extension)) {\n        return lang as MonacoLanguage;\n      }\n    }\n  }\n  return undefined;\n};\n\nexport const MonacoEditor = ({\n  value,\n  onValueChange,\n  language: _language,\n  readOnly,\n  filename,\n  minHeight,\n  className,\n}: {\n  value: string | undefined;\n  onValueChange?: (value: string) => void;\n  language: MonacoLanguage | undefined;\n  filename?: string;\n  readOnly?: boolean;\n  minHeight?: number;\n  className?: string;\n}) => {\n  const enable_fancy_toml =\n    useRead(\"GetCoreInfo\", {}).data?.enable_fancy_toml ?? false;\n  const language = (\n    _language === \"fancy_toml\" && !enable_fancy_toml ? \"toml\" : _language\n  ) as MonacoLanguage;\n  const dimensions = useWindowDimensions();\n  const [editor, setEditor] =\n    useState<monaco.editor.IStandaloneCodeEditor | null>(null);\n\n  useEffect(() => {\n    if (!editor) return;\n\n    let node = editor.getDomNode();\n    if (!node) return;\n\n    const callback = (e: any) => {\n      if (e.key === \"Escape\") {\n        (document.activeElement as any)?.blur?.();\n      }\n    };\n\n    node.addEventListener(\"keydown\", callback);\n    return () => node.removeEventListener(\"keydown\", callback);\n  }, [editor]);\n\n  useEffect(() => {\n    if (\n      language !== \"typescript\" &&\n      language !== \"javascript\" &&\n      language !== \"yaml\"\n    )\n      return;\n    if (!editor) return;\n    editor.addCommand(\n      monaco.KeyMod.Alt | monaco.KeyMod.Shift | monaco.KeyCode.KeyF,\n      async () => {\n        if (!editor) return;\n        const model = editor.getModel();\n        if (!model) return;\n        const position = editor.getPosition();\n        let beforeOffset = (position && model.getOffsetAt(position)) ?? 0;\n        const curr = editor.getValue();\n        const { formatted, cursorOffset } = await prettier.formatWithCursor(\n          curr,\n          {\n            cursorOffset: beforeOffset,\n            parser: language === \"yaml\" ? \"yaml\" : \"typescript\",\n            plugins:\n              language === \"yaml\"\n                ? [pluginYaml]\n                : [pluginTypescript, pluginEsTree as any],\n            printWidth: 80, // Set the desired max line length\n          }\n        );\n        editor.setValue(formatted);\n        editor.setPosition(model.getPositionAt(cursorOffset));\n      }\n    );\n  }, [editor]);\n\n  const line_count = value?.split(/\\r\\n|\\r|\\n/).length ?? 0;\n\n  useEffect(() => {\n    if (!editor) return;\n    const contentHeight = line_count * 18 + 30;\n    const containerNode = editor.getContainerDomNode();\n\n    containerNode.style.height = `${Math.max(\n      Math.min(contentHeight, Math.floor(dimensions.height * 0.5)),\n      minHeight ?? MIN_EDITOR_HEIGHT\n    )}px`;\n  }, [editor, line_count]);\n\n  const { currentTheme } = useTheme();\n\n  const options: monaco.editor.IStandaloneEditorConstructionOptions = {\n    minimap: { enabled: false },\n    // scrollbar: { alwaysConsumeMouseWheel: false },\n    scrollBeyondLastLine: false,\n    folding: false,\n    automaticLayout: true,\n    renderValidationDecorations: \"on\",\n    renderLineHighlightOnlyWhenFocus: true,\n    readOnly,\n    tabSize: 2,\n    detectIndentation: true,\n    quickSuggestions: true,\n    padding: {\n      top: 15,\n    },\n  };\n\n  return (\n    <div className={cn(\"mx-2 my-1 w-full\", className)}>\n      <Editor\n        language={language}\n        value={value}\n        theme={currentTheme}\n        defaultPath={defaultPath(filename)}\n        options={options}\n        onChange={(v) => onValueChange?.(v ?? \"\")}\n        onMount={(editor) => setEditor(editor)}\n      />\n    </div>\n  );\n};\n\nconst defaultPath = (filename?: string) => {\n  if (!filename) return undefined;\n  // Extract only the filename part of path,\n  // avoiding critical issue when path starts with '/'\n  const split = filename.split(\"/\");\n  return split[split.length - 1];\n};\n\nconst MIN_DIFF_HEIGHT = 100;\nconst MAX_DIFF_HEIGHT = 400;\n\nexport const MonacoDiffEditor = ({\n  original,\n  modified,\n  onModifiedValueChange,\n  language: _language,\n  readOnly,\n  containerClassName,\n  hideUnchangedRegions = true,\n}: {\n  original: string | undefined;\n  modified: string | undefined;\n  onModifiedValueChange?: (value: string) => void;\n  language: MonacoLanguage | undefined;\n  readOnly?: boolean;\n  containerClassName?: string;\n  hideUnchangedRegions?: boolean;\n}) => {\n  const enable_fancy_toml =\n    useRead(\"GetCoreInfo\", {}).data?.enable_fancy_toml ?? false;\n  const language = (\n    _language === \"fancy_toml\" && !enable_fancy_toml ? \"toml\" : _language\n  ) as MonacoLanguage;\n\n  const [editor, setEditor] =\n    useState<monaco.editor.IStandaloneDiffEditor | null>(null);\n\n  const original_line_count = original?.split(/\\r\\n|\\r|\\n/).length ?? 0;\n  const modified_line_count = modified?.split(/\\r\\n|\\r|\\n/).length ?? 0;\n  const line_count = Math.max(original_line_count, modified_line_count);\n\n  useEffect(() => {\n    if (!editor) return;\n    const contentHeight = line_count * 18 + 30;\n    const node = editor.getContainerDomNode();\n\n    node.style.height = `${Math.max(\n      Math.min(contentHeight, MAX_DIFF_HEIGHT),\n      MIN_DIFF_HEIGHT\n    )}px`;\n  }, [editor, line_count]);\n\n  const { currentTheme } = useTheme();\n\n  const options: monaco.editor.IStandaloneDiffEditorConstructionOptions = {\n    minimap: { enabled: true },\n    scrollbar: { alwaysConsumeMouseWheel: false },\n    scrollBeyondLastLine: false,\n    hideUnchangedRegions: { enabled: hideUnchangedRegions },\n    folding: false,\n    automaticLayout: true,\n    renderValidationDecorations: \"on\",\n    renderLineHighlightOnlyWhenFocus: true,\n    readOnly,\n    padding: {\n      top: 15,\n    },\n  };\n\n  return (\n    <div className={cn(\"mx-2 my-1\", containerClassName)}>\n      <DiffEditor\n        language={language}\n        original={original}\n        modified={modified}\n        theme={currentTheme}\n        options={options}\n        onMount={(editor) => {\n          const modifiedEditor = editor.getModifiedEditor();\n          modifiedEditor.onDidChangeModelContent((_) => {\n            onModifiedValueChange?.(modifiedEditor.getValue());\n          });\n          setEditor(editor);\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/omnibar.tsx",
    "content": "import { useAllResources, useLocalStorage, useRead, useSettingsView, useUser } from \"@lib/hooks\";\nimport { Button } from \"@ui/button\";\nimport {\n  CommandDialog,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandList,\n  CommandSeparator,\n  CommandItem,\n} from \"@ui/command\";\nimport { Box, Home, Search, User } from \"lucide-react\";\nimport { Fragment, ReactNode, useMemo, useState } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { cn, RESOURCE_TARGETS, usableResourcePath } from \"@lib/utils\";\nimport { Badge } from \"@ui/badge\";\nimport { ResourceComponents } from \"./resources\";\nimport { Switch } from \"@ui/switch\";\nimport { DOCKER_LINK_ICONS, TemplateMarker } from \"./util\";\nimport { UsableResource } from \"@types\";\n\nexport const OmniSearch = ({\n  className,\n  setOpen,\n}: {\n  className?: string;\n  setOpen: (open: boolean) => void;\n}) => {\n  return (\n    <Button\n      variant=\"outline\"\n      onClick={() => setOpen(true)}\n      className={cn(\n        \"flex items-center gap-4 w-fit md:w-[200px] lg:w-[300px] xl:w-[400px] justify-between hover:bg-card/50\",\n        className\n      )}\n    >\n      <div className=\"flex items-center gap-4\">\n        <Search className=\"w-4 h-4\" />{\" \"}\n        <span className=\"text-muted-foreground hidden md:flex\">Search</span>\n      </div>\n      <Badge\n        variant=\"outline\"\n        className=\"text-muted-foreground hidden md:inline-flex\"\n      >\n        shift + s\n      </Badge>\n    </Button>\n  );\n};\n\ntype OmniItem = {\n  key: string;\n  type: UsableResource;\n  label: string;\n  icon: ReactNode;\n  template: boolean;\n  onSelect: () => void;\n};\n\nexport const OmniDialog = ({\n  open,\n  setOpen,\n}: {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n}) => {\n  const [search, setSearch] = useState(\"\");\n  const navigate = useNavigate();\n  const nav = (value: string) => {\n    setOpen(false);\n    navigate(value);\n  };\n  const items = useOmniItems(nav, search);\n  const [showContainers, setShowContainers] = useLocalStorage(\n    \"omni-show-containers\",\n    false\n  );\n  return (\n    <CommandDialog open={open} onOpenChange={setOpen} manualFilter>\n      <CommandInput\n        placeholder=\"Search for resources...\"\n        value={search}\n        onValueChange={setSearch}\n      />\n      <div className=\"flex gap-2 text-xs items-center justify-end px-2 py-1\">\n        <div className=\"text-muted-foreground\">Show containers</div>\n        <Switch checked={showContainers} onCheckedChange={setShowContainers} />\n      </div>\n      <CommandList>\n        <CommandEmpty>No results found.</CommandEmpty>\n\n        {Object.entries(items)\n          .filter(([_, items]) => items.length > 0)\n          .map(([key, items], i) => (\n            <Fragment key={key}>\n              {i !== 0 && <CommandSeparator />}\n              <CommandGroup heading={key ? key : undefined}>\n                {items.map(({ key, type, label, icon, onSelect, template }) => (\n                  <CommandItem\n                    key={key}\n                    value={key}\n                    className=\"flex items-center gap-2 cursor-pointer\"\n                    onSelect={onSelect}\n                  >\n                    {icon}\n                    {label}\n                    {template && <TemplateMarker type={type} />}\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n            </Fragment>\n          ))}\n\n        {showContainers && (\n          <OmniContainers search={search} closeSearch={() => setOpen(false)} />\n        )}\n      </CommandList>\n    </CommandDialog>\n  );\n};\n\nconst useOmniItems = (\n  nav: (path: string) => void,\n  search: string\n): Record<string, OmniItem[]> => {\n  const user = useUser().data;\n  const resources = useAllResources();\n  const [_, setSettingsView] = useSettingsView();\n  return useMemo(() => {\n    const searchTerms = search\n      .toLowerCase()\n      .split(\" \")\n      .filter((term) => term);\n    return {\n      \"\": [\n        {\n          key: \"Home\",\n          type: \"Server\" as UsableResource,\n          label: \"Home\",\n          icon: <Home className=\"w-4 h-4\" />,\n          onSelect: () => nav(\"/\"),\n          template: false,\n        },\n        ...RESOURCE_TARGETS.map((_type) => {\n          const type = _type === \"ResourceSync\" ? \"Sync\" : _type;\n          const Components = ResourceComponents[_type];\n          return {\n            key: type + \"s\",\n            type: _type,\n            label: type + \"s\",\n            icon: <Components.Icon />,\n            onSelect: () => nav(usableResourcePath(_type)),\n            template: false,\n          };\n        }),\n        {\n          key: \"Containers\",\n          type: \"Server\" as UsableResource,\n          label: \"Containers\",\n          icon: <Box className=\"w-4 h-4\" />,\n          onSelect: () => nav(\"/containers\"),\n          template: false,\n        },\n        (user?.admin && {\n          key: \"Users\",\n          type: \"Server\" as UsableResource,\n          label: \"Users\",\n          icon: <User className=\"w-4 h-4\" />,\n          onSelect: () => {\n            setSettingsView(\"Users\");\n            nav(\"/settings\");\n          },\n          template: false,\n        }) as OmniItem,\n      ]\n        .filter((item) => item)\n        .filter((item) => {\n          const label = item.label.toLowerCase();\n          return (\n            searchTerms.length === 0 ||\n            searchTerms.every((term) => label.includes(term))\n          );\n        }),\n      ...Object.fromEntries(\n        RESOURCE_TARGETS.map((_type) => {\n          const type = _type === \"ResourceSync\" ? \"Sync\" : _type;\n          const lower_type = type.toLowerCase();\n          const Components = ResourceComponents[_type];\n          return [\n            type + \"s\",\n            resources[_type]\n              ?.filter((resource) => {\n                const lower_name = resource.name.toLowerCase();\n                return (\n                  searchTerms.length === 0 ||\n                  searchTerms.every(\n                    (term) =>\n                      lower_name.includes(term) || lower_type.includes(term)\n                  )\n                );\n              })\n              .map((resource) => ({\n                key: type + \"-\" + resource.name,\n                type: _type,\n                label: resource.name,\n                icon: <Components.Icon id={resource.id} />,\n                onSelect: () =>\n                  nav(`/${usableResourcePath(_type)}/${resource.id}`),\n                template: resource.template,\n              })) || [],\n          ];\n        })\n      ),\n    };\n  }, [user, resources, search]);\n};\n\nconst OmniContainers = ({\n  search,\n  closeSearch,\n}: {\n  search: string;\n  closeSearch: () => void;\n}) => {\n  const _containers = useRead(\"ListAllDockerContainers\", {}).data;\n  const containers = useMemo(() => {\n    return _containers?.filter((c) => {\n      const searchTerms = search\n        .toLowerCase()\n        .split(\" \")\n        .filter((term) => term);\n      if (searchTerms.length === 0) return true;\n      const lower = c.name.toLowerCase();\n      return searchTerms.every(\n        (term) => lower.includes(term) || \"containers\".includes(term)\n      );\n    });\n  }, [_containers, search]);\n  const navigate = useNavigate();\n  if ((containers?.length ?? 0) < 1) return null;\n  return (\n    <>\n      <CommandSeparator />\n      <CommandGroup heading=\"Containers\">\n        {containers?.map((container) => (\n          <CommandItem\n            key={container.id}\n            value={container.name}\n            className=\"flex items-center gap-2 cursor-pointer\"\n            onSelect={() => {\n              closeSearch();\n              navigate(\n                `/servers/${container.server_id!}/container/${container.name}`\n              );\n            }}\n          >\n            <DOCKER_LINK_ICONS.container\n              server_id={container.server_id!}\n              name={container.name}\n            />\n            {container.name}\n          </CommandItem>\n        ))}\n      </CommandGroup>\n    </>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/action/config.tsx",
    "content": "import {\n  useLocalStorage,\n  usePermissions,\n  useRead,\n  useWebhookIdOrName,\n  useWebhookIntegrations,\n  useWrite,\n} from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Config } from \"@components/config\";\nimport { MonacoEditor } from \"@components/monaco\";\nimport { SecretsSearch } from \"@components/config/env_vars\";\nimport { Button } from \"@ui/button\";\nimport {\n  ConfigItem,\n  ConfigSwitch,\n  WebhookBuilder,\n} from \"@components/config/util\";\nimport { Input } from \"@ui/input\";\nimport { useState } from \"react\";\nimport { CopyWebhook } from \"../common\";\nimport { ActionInfo } from \"./info\";\nimport { Switch } from \"@ui/switch\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { TimezoneSelector } from \"@components/util\";\nimport { snake_case_to_upper_space_case } from \"@lib/formatting\";\n\nconst ACTION_GIT_PROVIDER = \"Action\";\n\nexport const ActionConfig = ({ id }: { id: string }) => {\n  const [branch, setBranch] = useState(\"main\");\n  const { canWrite } = usePermissions({ type: \"Action\", id });\n  const action = useRead(\"GetAction\", { action: id }).data;\n  const config = action?.config;\n  const name = action?.name;\n  const global_disabled =\n    useRead(\"GetCoreInfo\", {}).data?.ui_write_disabled ?? false;\n  const [update, set] = useLocalStorage<Partial<Types.ActionConfig>>(\n    `action-${id}-update-v1`,\n    {}\n  );\n  const { mutateAsync } = useWrite(\"UpdateAction\");\n  const { integrations } = useWebhookIntegrations();\n  const [id_or_name] = useWebhookIdOrName();\n\n  if (!config) return null;\n\n  const disabled = global_disabled || !canWrite;\n  const webhook_integration = integrations[ACTION_GIT_PROVIDER] ?? \"Github\";\n\n  return (\n    <Config\n      disabled={disabled}\n      original={config}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: update });\n      }}\n      components={{\n        \"\": [\n          {\n            label: \"Action File\",\n            description: \"Manage the action file contents here.\",\n            components: {\n              file_contents: (file_contents, set) => {\n                return (\n                  <div className=\"flex flex-col gap-4\">\n                    <div className=\"flex items-center justify-between\">\n                      <SecretsSearch />\n                      <div className=\"hidden lg:flex items-center\">\n                        <div className=\"text-muted-foreground text-sm mr-2\">\n                          Docs:\n                        </div>\n                        {[\"read\", \"execute\", \"write\"].map((api) => (\n                          <a\n                            key={api}\n                            href={`https://docs.rs/komodo_client/latest/komodo_client/api/${api}/index.html`}\n                            target=\"_blank\"\n                          >\n                            <Button\n                              className=\"capitalize px-1\"\n                              size=\"sm\"\n                              variant=\"link\"\n                            >\n                              {api}\n                            </Button>\n                          </a>\n                        ))}\n                      </div>\n                    </div>\n                    <MonacoEditor\n                      value={file_contents}\n                      onValueChange={(file_contents) => set({ file_contents })}\n                      language=\"typescript\"\n                      readOnly={disabled}\n                    />\n                    <ActionInfo id={id} />\n                  </div>\n                );\n              },\n            },\n          },\n          {\n            label: \"Arguments\",\n            description: \"Manage the action file default arguments.\",\n            components: {\n              arguments: (args, set) => {\n                const format =\n                  update.arguments_format ??\n                  config.arguments_format ??\n                  Types.FileFormat.KeyValue;\n                return (\n                  <div className=\"flex flex-col gap-4\">\n                    <div className=\"flex items-center gap-4\">\n                      <SecretsSearch />\n                      <Select\n                        value={format}\n                        onValueChange={(arguments_format: Types.FileFormat) =>\n                          set({ arguments_format })\n                        }\n                      >\n                        <SelectTrigger className=\"w-fit\">\n                          <div className=\"flex gap-2 items-center mr-2\">\n                            <div className=\"text-muted-foreground\">Format:</div>\n                            <SelectValue />\n                          </div>\n                        </SelectTrigger>\n                        <SelectContent>\n                          {Object.values(Types.FileFormat)\n                            // Don't allow selection of Toml, as this option will break resource sync\n                            .filter((f) => f !== Types.FileFormat.Toml)\n                            .map((format) => (\n                              <SelectItem value={format}>\n                                {snake_case_to_upper_space_case(format)}\n                              </SelectItem>\n                            ))}\n                        </SelectContent>\n                      </Select>\n                    </div>\n                    <MonacoEditor\n                      value={args || default_arguments(format)}\n                      onValueChange={(args) => set({ arguments: args })}\n                      language={\n                        update.arguments_format ??\n                        config.arguments_format ??\n                        Types.FileFormat.KeyValue\n                      }\n                      readOnly={disabled}\n                    />\n                  </div>\n                );\n              },\n            },\n          },\n\n          {\n            label: \"Alert\",\n            labelHidden: true,\n            components: {\n              failure_alert: {\n                boldLabel: true,\n                description: \"Send an alert any time the Procedure fails\",\n              },\n            },\n          },\n          {\n            label: \"Schedule\",\n            description:\n              \"Configure the Procedure to run at defined times using English or CRON.\",\n            components: {\n              schedule_enabled: (schedule_enabled, set) => (\n                <ConfigSwitch\n                  label=\"Enabled\"\n                  value={\n                    (update.schedule ?? config.schedule)\n                      ? schedule_enabled\n                      : false\n                  }\n                  disabled={disabled || !(update.schedule ?? config.schedule)}\n                  onChange={(schedule_enabled) => set({ schedule_enabled })}\n                />\n              ),\n              schedule_format: (schedule_format, set) => (\n                <ConfigItem\n                  label=\"Format\"\n                  description=\"Choose whether to provide English or CRON schedule expression\"\n                >\n                  <Select\n                    value={schedule_format}\n                    onValueChange={(schedule_format) =>\n                      set({\n                        schedule_format:\n                          schedule_format as Types.ScheduleFormat,\n                      })\n                    }\n                    disabled={disabled}\n                  >\n                    <SelectTrigger className=\"w-[200px]\" disabled={disabled}>\n                      <SelectValue placeholder=\"Select Format\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      {Object.values(Types.ScheduleFormat).map((mode) => (\n                        <SelectItem\n                          key={mode}\n                          value={mode!}\n                          className=\"cursor-pointer\"\n                        >\n                          {mode}\n                        </SelectItem>\n                      ))}\n                    </SelectContent>\n                  </Select>\n                </ConfigItem>\n              ),\n              schedule: {\n                label: \"Expression\",\n                description:\n                  (update.schedule_format ?? config.schedule_format) ===\n                  \"Cron\" ? (\n                    <div className=\"pt-1 flex flex-col gap-1\">\n                      <code>\n                        second - minute - hour - day - month - day-of-week\n                      </code>\n                    </div>\n                  ) : (\n                    <div className=\"pt-1 flex flex-col gap-1\">\n                      <code>Examples:</code>\n                      <code>- Run every day at 4:00 pm</code>\n                      <code>\n                        - Run at 21:00 on the 1st and 15th of the month\n                      </code>\n                      <code>- Every Sunday at midnight</code>\n                    </div>\n                  ),\n                placeholder:\n                  (update.schedule_format ?? config.schedule_format) === \"Cron\"\n                    ? \"0 0 0 ? * SUN\"\n                    : \"Enter English expression\",\n              },\n              schedule_timezone: (timezone, set) => {\n                return (\n                  <ConfigItem\n                    label=\"Timezone\"\n                    description=\"Select specific IANA timezone for schedule expression.\"\n                  >\n                    <TimezoneSelector\n                      timezone={timezone ?? \"\"}\n                      onChange={(schedule_timezone) =>\n                        set({ schedule_timezone })\n                      }\n                      disabled={disabled}\n                    />\n                  </ConfigItem>\n                );\n              },\n              schedule_alert: {\n                description: \"Send an alert when the scheduled run occurs\",\n              },\n            },\n          },\n          {\n            label: \"Startup\",\n            labelHidden: true,\n            components: {\n              run_at_startup: {\n                label: \"Run on Startup\",\n                description:\n                  \"Run this action on completion of startup of Komodo Core\",\n              },\n            },\n          },\n          {\n            label: \"Reload\",\n            labelHidden: true,\n            components: {\n              reload_deno_deps: {\n                label: \"Reload Dependencies\",\n                description:\n                  \"Whether deno will be instructed to reload all dependencies. This can usually be kept disabled outside of development.\",\n              },\n            },\n          },\n          {\n            label: \"Webhook\",\n            description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,\n            components: {\n              [\"Builder\" as any]: () => (\n                <WebhookBuilder git_provider={ACTION_GIT_PROVIDER}>\n                  <div className=\"text-nowrap text-muted-foreground text-sm\">\n                    Listen on branch:\n                  </div>\n                  <div className=\"flex items-center gap-3\">\n                    <Input\n                      placeholder=\"Branch\"\n                      value={branch}\n                      onChange={(e) => setBranch(e.target.value)}\n                      className=\"w-[200px]\"\n                      disabled={branch === \"__ANY__\"}\n                    />\n                    <div className=\"flex items-center gap-2\">\n                      <div className=\"text-muted-foreground text-sm\">\n                        No branch check:\n                      </div>\n                      <Switch\n                        checked={branch === \"__ANY__\"}\n                        onCheckedChange={(checked) => {\n                          if (checked) {\n                            setBranch(\"__ANY__\");\n                          } else {\n                            setBranch(\"main\");\n                          }\n                        }}\n                      />\n                    </div>\n                  </div>\n                </WebhookBuilder>\n              ),\n              [\"run\" as any]: () => (\n                <ConfigItem label=\"Webhook Url - Run\">\n                  <CopyWebhook\n                    integration={webhook_integration}\n                    path={`/action/${id_or_name === \"Id\" ? id : encodeURIComponent(name ?? \"...\")}/${branch}`}\n                  />\n                </ConfigItem>\n              ),\n              webhook_enabled: true,\n              webhook_secret: {\n                description:\n                  \"Provide a custom webhook secret for this resource, or use the global default.\",\n                placeholder: \"Input custom secret\",\n              },\n            },\n          },\n        ],\n      }}\n    />\n  );\n};\n\nconst default_arguments = (format: Types.FileFormat) => {\n  switch (format) {\n    case Types.FileFormat.KeyValue:\n      return \"# ARG_NAME = value\\n\";\n    case Types.FileFormat.Toml:\n      return '# ARG_NAME = \"value\"\\n';\n    case Types.FileFormat.Yaml:\n      return \"# ARG_NAME: value\\n\";\n    case Types.FileFormat.Json:\n      return \"{}\\n\";\n  }\n};\n"
  },
  {
    "path": "frontend/src/components/resources/action/index.tsx",
    "content": "import { ActionWithDialog, StatusBadge } from \"@components/util\";\nimport { useExecute, useRead } from \"@lib/hooks\";\nimport { RequiredResourceComponents } from \"@types\";\nimport { Clapperboard, Clock } from \"lucide-react\";\nimport { ActionConfig } from \"./config\";\nimport { ActionTable } from \"./table\";\nimport { DeleteResource, NewResource, ResourcePageHeader } from \"../common\";\nimport {\n  action_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\nimport { cn, updateLogToHtml } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { DashboardPieChart } from \"@pages/home/dashboard\";\nimport { GroupActions } from \"@components/group-actions\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@ui/tooltip\";\nimport { Card } from \"@ui/card\";\n\nconst useAction = (id?: string) =>\n  useRead(\"ListActions\", {}).data?.find((d) => d.id === id);\n\nconst ActionIcon = ({ id, size }: { id?: string; size: number }) => {\n  const state = useAction(id)?.info.state;\n  const color = stroke_color_class_by_intention(action_state_intention(state));\n  return <Clapperboard className={cn(`w-${size} h-${size}`, state && color)} />;\n};\n\nexport const ActionComponents: RequiredResourceComponents = {\n  list_item: (id) => useAction(id),\n  resource_links: () => undefined,\n\n  Description: () => <>Custom scripts using the Komodo client.</>,\n\n  Dashboard: () => {\n    const summary = useRead(\"GetActionsSummary\", {}).data;\n    return (\n      <DashboardPieChart\n        data={[\n          { title: \"Ok\", intention: \"Good\", value: summary?.ok ?? 0 },\n          {\n            title: \"Running\",\n            intention: \"Warning\",\n            value: summary?.running ?? 0,\n          },\n          {\n            title: \"Failed\",\n            intention: \"Critical\",\n            value: summary?.failed ?? 0,\n          },\n          {\n            title: \"Unknown\",\n            intention: \"Unknown\",\n            value: summary?.unknown ?? 0,\n          },\n        ]}\n      />\n    );\n  },\n\n  New: () => <NewResource type=\"Action\" />,\n\n  GroupActions: () => <GroupActions type=\"Action\" actions={[\"RunAction\"]} />,\n\n  Table: ({ resources }) => (\n    <ActionTable actions={resources as Types.ActionListItem[]} />\n  ),\n\n  Icon: ({ id }) => <ActionIcon id={id} size={4} />,\n  BigIcon: ({ id }) => <ActionIcon id={id} size={8} />,\n\n  State: ({ id }) => {\n    let state = useAction(id)?.info.state;\n    return <StatusBadge text={state} intent={action_state_intention(state)} />;\n  },\n\n  Status: {},\n\n  Info: {\n    Schedule: ({ id }) => {\n      const next_scheduled_run = useAction(id)?.info.next_scheduled_run;\n      return (\n        <div className=\"flex gap-2 items-center\">\n          <Clock className=\"w-4 h-4\" />\n          Next Run:\n          <div className=\"font-bold\">\n            {next_scheduled_run\n              ? new Date(next_scheduled_run).toLocaleString()\n              : \"Not Scheduled\"}\n          </div>\n        </div>\n      );\n    },\n    ScheduleErrors: ({ id }) => {\n      const error = useAction(id)?.info.schedule_error;\n      if (!error) {\n        return null;\n      }\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card className=\"px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer\">\n              <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                Schedule Error\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent className=\"w-[400px]\">\n            <pre\n              dangerouslySetInnerHTML={{\n                __html: updateLogToHtml(error),\n              }}\n              className=\"max-h-[500px] overflow-y-auto\"\n            />\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n  },\n\n  Actions: {\n    RunAction: ({ id }) => {\n      const running =\n        (useRead(\n          \"GetActionActionState\",\n          { action: id },\n          { refetchInterval: 5000 }\n        ).data?.running ?? 0) > 0;\n      const { mutate, isPending } = useExecute(\"RunAction\");\n      const action = useAction(id);\n      if (!action) return null;\n      return (\n        <ActionWithDialog\n          name={action.name}\n          title={running ? \"Running\" : \"Run Action\"}\n          icon={<Clapperboard className=\"h-4 w-4\" />}\n          onClick={() => mutate({ action: id, args: {} })}\n          disabled={running || isPending}\n          loading={running}\n        />\n      );\n    },\n  },\n\n  Page: {},\n\n  Config: ActionConfig,\n\n  DangerZone: ({ id }) => <DeleteResource type=\"Action\" id={id} />,\n\n  ResourcePageHeader: ({ id }) => {\n    const action = useAction(id);\n    return (\n      <ResourcePageHeader\n        intent={action_state_intention(action?.info.state)}\n        icon={<ActionIcon id={id} size={8} />}\n        type=\"Action\"\n        id={id}\n        resource={action}\n        state={action?.info.state}\n        status={undefined}\n      />\n    );\n  },\n};\n"
  },
  {
    "path": "frontend/src/components/resources/action/info.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { Card, CardContent, CardHeader } from \"@ui/card\";\nimport { cn, getUpdateQuery, updateLogToHtml } from \"@lib/utils\";\nimport { useRead } from \"@lib/hooks\";\nimport { text_color_class_by_intention } from \"@lib/color\";\n\nexport const ActionInfo = ({ id }: { id: string }) => {\n  const update = useRead(\"ListUpdates\", {\n    query: {\n      ...getUpdateQuery({ type: \"Action\", id }, undefined),\n      operation: \"RunAction\",\n    },\n  }).data?.updates[0];\n\n  const full_update = useRead(\n    \"GetUpdate\",\n    { id: update?.id! },\n    { enabled: !!update?.id }\n  ).data;\n\n  const log = full_update?.logs.find((log) => log.stage === \"Execute Action\");\n\n  if (!log?.stdout && !log?.stderr) {\n    return (\n      <Section>\n        <Card className=\"flex flex-col gap-4\">\n          <CardHeader\n            className={cn(\n              \"flex flex-row justify-between items-center\",\n              text_color_class_by_intention(\"Neutral\")\n            )}\n          >\n            Never run\n          </CardHeader>\n        </Card>\n      </Section>\n    );\n  }\n\n  return (\n    <Section>\n      {/* Last run */}\n      {log?.stdout && (\n        <Card className=\"flex flex-col gap-4\">\n          <CardHeader className=\"flex flex-row items-center gap-1 pb-0\">\n            Last run -\n            <div className={text_color_class_by_intention(\"Good\")}>Stdout</div>\n          </CardHeader>\n          <CardContent className=\"pr-8\">\n            <pre\n              dangerouslySetInnerHTML={{\n                __html: updateLogToHtml(log.stdout),\n              }}\n              className=\"max-h-[500px] overflow-y-auto\"\n            />\n          </CardContent>\n        </Card>\n      )}\n      {log?.stderr && (\n        <Card className=\"flex flex-col gap-4\">\n          <CardHeader className=\"flex flex-row items-center gap-1 pb-0\">\n            Last run -\n            <div className={text_color_class_by_intention(\"Critical\")}>\n              Stderr\n            </div>\n          </CardHeader>\n          <CardContent className=\"pr-8\">\n            <pre\n              dangerouslySetInnerHTML={{\n                __html: updateLogToHtml(log.stderr),\n              }}\n              className=\"max-h-[500px] overflow-y-auto\"\n            />\n          </CardContent>\n        </Card>\n      )}\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/action/table.tsx",
    "content": "import { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { TableTags } from \"@components/tags\";\nimport { ResourceLink } from \"../common\";\nimport { ActionComponents } from \".\";\nimport { Types } from \"komodo_client\";\nimport { useSelectedResources } from \"@lib/hooks\";\n\nexport const ActionTable = ({\n  actions,\n}: {\n  actions: Types.ActionListItem[];\n}) => {\n  const [_, setSelectedResources] = useSelectedResources(\"Action\");\n\n  return (\n    <DataTable\n      tableKey=\"actions\"\n      data={actions}\n      selectOptions={{\n        selectKey: ({ name }) => name,\n        onSelect: setSelectedResources,\n      }}\n      columns={[\n        {\n          accessorKey: \"name\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Name\" />\n          ),\n          cell: ({ row }) => (\n            <ResourceLink type=\"Action\" id={row.original.id} />\n          ),\n        },\n        {\n          accessorKey: \"info.state\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"State\" />\n          ),\n          cell: ({ row }) => <ActionComponents.State id={row.original.id} />,\n        },\n        {\n          accessorKey: \"info.next_scheduled_run\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Next Run\" />\n          ),\n          sortingFn: (a, b) => {\n            const sa = a.original.info.next_scheduled_run;\n            const sb = b.original.info.next_scheduled_run;\n\n            if (!sa && !sb) return 0;\n            if (!sa) return 1;\n            if (!sb) return -1;\n\n            if (sa > sb) return 1;\n            else if (sa < sb) return -1;\n            else return 0;\n          },\n          cell: ({ row }) =>\n            row.original.info.next_scheduled_run\n              ? new Date(row.original.info.next_scheduled_run).toLocaleString()\n              : \"Not Scheduled\",\n        },\n        {\n          header: \"Tags\",\n          cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/alerter/config/alert_types.tsx",
    "content": "import { ConfigItem } from \"@components/config/util\";\nimport { Types } from \"komodo_client\";\nimport { Badge } from \"@ui/badge\";\nimport { Select, SelectContent, SelectItem, SelectTrigger } from \"@ui/select\";\nimport { MinusCircle } from \"lucide-react\";\n\nconst ALERT_TYPES: Types.AlertData[\"type\"][] = [\n  // Server\n  \"ServerVersionMismatch\",\n  \"ServerUnreachable\",\n  \"ServerCpu\",\n  \"ServerMem\",\n  \"ServerDisk\",\n  // Stack\n  \"StackStateChange\",\n  \"StackImageUpdateAvailable\",\n  \"StackAutoUpdated\",\n  // Deployment\n  \"ContainerStateChange\",\n  \"DeploymentImageUpdateAvailable\",\n  \"DeploymentAutoUpdated\",\n  // Misc\n  \"ScheduleRun\",\n  \"BuildFailed\",\n  \"ResourceSyncPendingUpdates\",\n  \"RepoBuildFailed\",\n  \"ActionFailed\",\n  \"ProcedureFailed\",\n  \"AwsBuilderTerminationFailed\",\n  \"Custom\",\n];\n\nexport const AlertTypeConfig = ({\n  alert_types,\n  set,\n  disabled,\n}: {\n  alert_types: Types.AlertData[\"type\"][];\n  set: (alert_types: Types.AlertData[\"type\"][]) => void;\n  disabled: boolean;\n}) => {\n  const at = ALERT_TYPES.filter(\n    (alert_type) => !alert_types.includes(alert_type),\n  );\n  return (\n    <ConfigItem\n      label=\"Alert Types\"\n      description=\"Only send alerts of certain types.\"\n      boldLabel\n    >\n      <div className=\"flex items-center gap-4\">\n        {at.length ? (\n          <Select\n            value={undefined}\n            onValueChange={(type: Types.AlertData[\"type\"]) => {\n              set([...alert_types, type]);\n            }}\n            disabled={disabled}\n          >\n            <SelectTrigger className=\"w-[150px]\">\n              <div className=\"pr-2\">Add Filter</div>\n            </SelectTrigger>\n            <SelectContent align=\"start\">\n              {at.map((alert_type) => (\n                <SelectItem key={alert_type} value={alert_type}>\n                  {alert_type}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        ) : undefined}\n        <div className=\"flex items-center flex-wrap gap-2 w-[75%]\">\n          {alert_types.map((type) => (\n            <Badge\n              variant=\"secondary\"\n              className=\"text-sm flex items-center gap-2 cursor-pointer\"\n              onClick={() => {\n                if (disabled) return;\n                set(alert_types.filter((t) => t !== type));\n              }}\n            >\n              {type}\n              {!disabled && <MinusCircle className=\"w-3 h-3\" />}\n            </Badge>\n          ))}\n        </div>\n      </div>\n    </ConfigItem>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/alerter/config/endpoint.tsx",
    "content": "import { ConfigItem } from \"@components/config/util\";\nimport { MonacoEditor } from \"@components/monaco\";\nimport { Types } from \"komodo_client\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { Input } from \"@ui/input\";\n\nconst ENDPOINT_TYPES: Types.AlerterEndpoint[\"type\"][] = [\n  \"Custom\",\n  \"Discord\",\n  \"Slack\",\n  \"Ntfy\",\n  \"Pushover\",\n];\n\nexport const EndpointConfig = ({\n  endpoint,\n  set,\n  disabled,\n}: {\n  endpoint: Types.AlerterEndpoint;\n  set: (endpoint: Types.AlerterEndpoint) => void;\n  disabled: boolean;\n}) => {\n  return (\n    <ConfigItem\n      label=\"Endpoint\"\n      description=\"Configure the endpoint to send the alert to.\"\n      boldLabel\n    >\n      <Select\n        value={endpoint.type}\n        onValueChange={(type: Types.AlerterEndpoint[\"type\"]) => {\n          set({ type, params: { url: default_url(type) } });\n        }}\n        disabled={disabled}\n      >\n        <SelectTrigger className=\"w-[150px]\" disabled={disabled}>\n          <SelectValue />\n        </SelectTrigger>\n        <SelectContent>\n          {ENDPOINT_TYPES.map((endpoint) => (\n            <SelectItem key={endpoint} value={endpoint}>\n              {endpoint}\n            </SelectItem>\n          ))}\n        </SelectContent>\n      </Select>\n      <MonacoEditor\n        value={endpoint.params.url}\n        language={undefined}\n        onValueChange={(url) =>\n          set({ ...endpoint, params: { ...endpoint.params, url } })\n        }\n        readOnly={disabled}\n      />\n      {endpoint.type == \"Ntfy\" ? (\n        <ConfigItem\n          label=\"Email\"\n          description=\"Request Ntfy to send an email to this address. SMTP must be configured on the Ntfy instance. Only one email address per alerter is supported.\"\n        >\n          <Input\n            value={endpoint.params.email}\n            type=\"email\"\n            readOnly={disabled}\n            placeholder=\"john@example.com\"\n            onChange={(input) =>\n              set({\n                ...endpoint,\n                params: { ...endpoint.params, email: input.target.value },\n              })\n            }\n          ></Input>\n        </ConfigItem>\n      ) : (\n        \"\"\n      )}\n    </ConfigItem>\n  );\n};\n\nconst default_url = (type: Types.AlerterEndpoint[\"type\"]) => {\n  return type === \"Custom\"\n    ? \"http://localhost:7000\"\n    : type === \"Slack\"\n      ? \"https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX\"\n      : type === \"Discord\"\n        ? \"https://discord.com/api/webhooks/XXXXXXXXXXXX/XXXX-XXXXXXXXXX\"\n        : type === \"Ntfy\"\n          ? \"https://ntfy.sh/komodo\"\n          : type === \"Pushover\"\n            ? \"https://api.pushover.net/1/messages.json?token=XXXXXXXXXXXXX&user=XXXXXXXXXXXXX\"\n            : \"\";\n};\n"
  },
  {
    "path": "frontend/src/components/resources/alerter/config/index.tsx",
    "content": "import { Config } from \"@components/config\";\nimport { useLocalStorage, usePermissions, useRead, useWrite } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { EndpointConfig } from \"./endpoint\";\nimport { AlertTypeConfig } from \"./alert_types\";\nimport { ResourcesConfig } from \"./resources\";\nimport { MaintenanceWindows } from \"@components/config/maintenance\";\n\nexport const AlerterConfig = ({ id }: { id: string }) => {\n  const { canWrite } = usePermissions({ type: \"Alerter\", id });\n  const config = useRead(\"GetAlerter\", { alerter: id }).data?.config;\n  const global_disabled =\n    useRead(\"GetCoreInfo\", {}).data?.ui_write_disabled ?? false;\n  const { mutateAsync } = useWrite(\"UpdateAlerter\");\n  const [update, set] = useLocalStorage<Partial<Types.AlerterConfig>>(\n    `alerter-${id}-update-v1`,\n    {}\n  );\n\n  if (!config) return null;\n  const disabled = global_disabled || !canWrite;\n\n  return (\n    <Config\n      disabled={disabled}\n      original={config}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: update });\n      }}\n      components={{\n        \"\": [\n          {\n            label: \"Enabled\",\n            labelHidden: true,\n            components: {\n              enabled: {\n                boldLabel: true,\n                description: \"Whether to send alerts to the endpoint.\",\n              },\n            },\n          },\n          {\n            label: \"Endpoint\",\n            labelHidden: true,\n            components: {\n              endpoint: (endpoint, set) => (\n                <EndpointConfig\n                  endpoint={endpoint!}\n                  set={(endpoint) => set({ endpoint })}\n                  disabled={disabled}\n                />\n              ),\n            },\n          },\n          {\n            label: \"Filter\",\n            labelHidden: true,\n            components: {\n              alert_types: (alert_types, set) => (\n                <AlertTypeConfig\n                  alert_types={alert_types!}\n                  set={(alert_types) => set({ alert_types })}\n                  disabled={disabled}\n                />\n              ),\n              resources: (resources, set) => (\n                <ResourcesConfig\n                  resources={resources!}\n                  set={(resources) => set({ resources })}\n                  disabled={disabled}\n                  blacklist={false}\n                />\n              ),\n              except_resources: (resources, set) => (\n                <ResourcesConfig\n                  resources={resources!}\n                  set={(except_resources) => set({ except_resources })}\n                  disabled={disabled}\n                  blacklist={true}\n                />\n              ),\n            },\n          },\n          {\n            label: \"Maintenance\",\n            boldLabel: false,\n            description: (\n              <>\n                Configure maintenance windows to temporarily disable alerts\n                during scheduled maintenance periods. When a maintenance window\n                is active, alerts which would be sent by this alerter will be\n                suppressed.\n              </>\n            ),\n            components: {\n              maintenance_windows: (values, set) => {\n                return (\n                  <MaintenanceWindows\n                    windows={values ?? []}\n                    onUpdate={(maintenance_windows) =>\n                      set({ maintenance_windows })\n                    }\n                    disabled={disabled}\n                  />\n                );\n              },\n            },\n          },\n        ],\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/alerter/config/resources.tsx",
    "content": "import { ConfigItem } from \"@components/config/util\";\nimport { ResourceComponents } from \"@components/resources\";\nimport { ResourceLink } from \"@components/resources/common\";\nimport { useRead } from \"@lib/hooks\";\nimport { resource_name } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { UsableResource } from \"@types\";\nimport { Button } from \"@ui/button\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { Input } from \"@ui/input\";\nimport { Switch } from \"@ui/switch\";\nimport { useState } from \"react\";\n\nexport const ResourcesConfig = ({\n  resources,\n  set,\n  disabled,\n  blacklist,\n}: {\n  resources: Types.ResourceTarget[];\n  set: (resources: Types.ResourceTarget[]) => void;\n  disabled: boolean;\n  blacklist: boolean;\n}) => {\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const servers = useRead(\"ListServers\", {}).data ?? [];\n  const stacks = useRead(\"ListStacks\", {}).data ?? [];\n  const deployments = useRead(\"ListDeployments\", {}).data ?? [];\n  const builds = useRead(\"ListBuilds\", {}).data ?? [];\n  const repos = useRead(\"ListRepos\", {}).data ?? [];\n  const syncs = useRead(\"ListResourceSyncs\", {}).data ?? [];\n  const all_resources = [\n    ...servers.map((server) => {\n      return {\n        type: \"Server\",\n        id: server.id,\n        name: server.name.toLowerCase(),\n        enabled: resources.find(\n          (r) => r.type === \"Server\" && r.id === server.id\n        )\n          ? true\n          : false,\n      };\n    }),\n    ...stacks.map((stack) => {\n      return {\n        type: \"Stack\",\n        id: stack.id,\n        name: stack.name.toLowerCase(),\n        enabled: resources.find((r) => r.type === \"Stack\" && r.id === stack.id)\n          ? true\n          : false,\n      };\n    }),\n    ...deployments.map((deployment) => ({\n      type: \"Deployment\",\n      id: deployment.id,\n      name: deployment.name.toLowerCase(),\n      enabled: resources.find(\n        (r) => r.type === \"Deployment\" && r.id === deployment.id\n      )\n        ? true\n        : false,\n    })),\n    ...builds.map((build) => ({\n      type: \"Build\",\n      id: build.id,\n      name: build.name.toLowerCase(),\n      enabled: resources.find((r) => r.type === \"Build\" && r.id === build.id)\n        ? true\n        : false,\n    })),\n    ...repos.map((repo) => ({\n      type: \"Repo\",\n      id: repo.id,\n      name: repo.name.toLowerCase(),\n      enabled: resources.find((r) => r.type === \"Repo\" && r.id === repo.id)\n        ? true\n        : false,\n    })),\n    ...syncs.map((sync) => ({\n      type: \"ResourceSync\",\n      id: sync.id,\n      name: sync.name.toLowerCase(),\n      enabled: resources.find(\n        (r) => r.type === \"ResourceSync\" && r.id === sync.id\n      )\n        ? true\n        : false,\n    })),\n  ];\n  const searchSplit = search.split(\" \");\n  const filtered_resources = searchSplit.length\n    ? all_resources.filter((r) => {\n        const name = r.name.toLowerCase();\n        return searchSplit.every((term) => name.includes(term));\n      })\n    : all_resources;\n  return (\n    <ConfigItem label={`Resource ${blacklist ? \"Blacklist\" : \"Whitelist\"}`}>\n      <div className=\"flex items-center gap-4\">\n        <Dialog open={open} onOpenChange={setOpen}>\n          <DialogTrigger>\n            <Button variant=\"secondary\">Edit Resources</Button>\n          </DialogTrigger>\n          <DialogContent className=\"min-w-[90vw] xl:min-w-[1200px]\">\n            <DialogHeader>Alerter Resources</DialogHeader>\n            <div className=\"flex flex-col gap-4\">\n              <Input\n                value={search}\n                onChange={(e) => setSearch(e.target.value)}\n                placeholder=\"Search...\"\n                className=\"w-[200px] lg:w-[300px]\"\n              />\n              <div className=\"max-h-[70vh] overflow-auto\">\n                <DataTable\n                  tableKey=\"alerter-resources\"\n                  data={filtered_resources}\n                  columns={[\n                    {\n                      accessorKey: \"type\",\n                      header: ({ column }) => (\n                        <SortableHeader column={column} title=\"Resource\" />\n                      ),\n                      cell: ({ row }) => {\n                        const Components =\n                          ResourceComponents[\n                            row.original.type as UsableResource\n                          ];\n                        return (\n                          <div className=\"flex gap-2 items-center\">\n                            <Components.Icon />\n                            {row.original.type}\n                          </div>\n                        );\n                      },\n                    },\n                    {\n                      accessorKey: \"id\",\n                      sortingFn: (a, b) => {\n                        const ra = resource_name(\n                          a.original.type as UsableResource,\n                          a.original.id\n                        );\n                        const rb = resource_name(\n                          b.original.type as UsableResource,\n                          b.original.id\n                        );\n\n                        if (!ra && !rb) return 0;\n                        if (!ra) return -1;\n                        if (!rb) return 1;\n\n                        if (ra > rb) return 1;\n                        else if (ra < rb) return -1;\n                        else return 0;\n                      },\n                      header: ({ column }) => (\n                        <SortableHeader column={column} title=\"Target\" />\n                      ),\n                      cell: ({ row: { original: resource_target } }) => {\n                        return (\n                          <ResourceLink\n                            type={resource_target.type as UsableResource}\n                            id={resource_target.id}\n                          />\n                        );\n                      },\n                    },\n                    {\n                      accessorKey: \"enabled\",\n                      header: ({ column }) => (\n                        <SortableHeader\n                          column={column}\n                          title={blacklist ? \"Blacklist\" : \"Whitelist\"}\n                        />\n                      ),\n                      cell: ({ row }) => {\n                        return (\n                          <Switch\n                            disabled={disabled}\n                            checked={row.original.enabled}\n                            onCheckedChange={() => {\n                              if (row.original.enabled) {\n                                set(\n                                  resources.filter(\n                                    (r) =>\n                                      r.type !== row.original.type ||\n                                      r.id !== row.original.id\n                                  )\n                                );\n                              } else {\n                                set([\n                                  ...resources,\n                                  {\n                                    type: row.original.type as UsableResource,\n                                    id: row.original.id,\n                                  },\n                                ]);\n                              }\n                            }}\n                          />\n                        );\n                      },\n                    },\n                  ]}\n                />\n              </div>\n            </div>\n            <DialogFooter>\n              <Button onClick={() => setOpen(false)}>Confirm</Button>\n            </DialogFooter>\n          </DialogContent>\n        </Dialog>\n        {resources.length ? (\n          <div className=\"text-muted-foreground\">\n            Alerts {blacklist ? \"blacklisted\" : \"whitelisted\"} by{\" \"}\n            {resources.length} resources\n          </div>\n        ) : undefined}\n      </div>\n    </ConfigItem>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/alerter/index.tsx",
    "content": "import { useExecute, useRead, useUser } from \"@lib/hooks\";\nimport { RequiredResourceComponents } from \"@types\";\nimport { AlarmClock, FlaskConical } from \"lucide-react\";\nimport { Link } from \"react-router-dom\";\nimport { Card, CardDescription, CardHeader, CardTitle } from \"@ui/card\";\nimport { AlerterConfig } from \"./config\";\nimport { DeleteResource, NewResource, ResourcePageHeader } from \"../common\";\nimport { AlerterTable } from \"./table\";\nimport { Types } from \"komodo_client\";\nimport { ConfirmButton } from \"@components/util\";\nimport { GroupActions } from \"@components/group-actions\";\n\nconst useAlerter = (id?: string) =>\n  useRead(\"ListAlerters\", {}).data?.find((d) => d.id === id);\n\nexport const AlerterComponents: RequiredResourceComponents = {\n  list_item: (id) => useAlerter(id),\n  resource_links: () => undefined,\n\n  Description: () => <>Route alerts to various endpoints.</>,\n\n  Dashboard: () => {\n    const alerters_count = useRead(\"ListAlerters\", {}).data?.length;\n    return (\n      <Link to=\"/alerters/\" className=\"w-full\">\n        <Card className=\"hover:bg-accent/50 transition-colors cursor-pointer\">\n          <CardHeader>\n            <div className=\"flex justify-between\">\n              <div>\n                <CardTitle>Alerters</CardTitle>\n                <CardDescription>{alerters_count} Total</CardDescription>\n              </div>\n              <AlarmClock className=\"w-4 h-4\" />\n            </div>\n          </CardHeader>\n        </Card>\n      </Link>\n    );\n  },\n\n  New: () => {\n    const is_admin = useUser().data?.admin;\n    return is_admin && <NewResource type=\"Alerter\" />;\n  },\n\n  GroupActions: () => <GroupActions type=\"Alerter\" actions={[\"TestAlerter\"]} />,\n\n  Table: ({ resources }) => (\n    <AlerterTable alerters={resources as Types.AlerterListItem[]} />\n  ),\n\n  Icon: () => <AlarmClock className=\"w-4 h-4\" />,\n  BigIcon: () => <AlarmClock className=\"w-8 h-8\" />,\n\n  State: () => null,\n  Status: {},\n\n  Info: {\n    Type: ({ id }) => {\n      const alerter = useAlerter(id);\n      return (\n        <div className=\"capitalize\">Type: {alerter?.info.endpoint_type}</div>\n      );\n    },\n  },\n\n  Actions: {\n    TestAlerter: ({ id }) => {\n      const { mutate, isPending } = useExecute(\"TestAlerter\");\n      const alerter = useAlerter(id);\n      if (!alerter) return null;\n      return (\n        <ConfirmButton\n          title=\"Test Alerter\"\n          icon={<FlaskConical className=\"h-4 w-4\" />}\n          loading={isPending}\n          onClick={() => mutate({ alerter: id })}\n          disabled={isPending}\n        />\n      );\n    },\n  },\n\n  Page: {},\n\n  Config: AlerterConfig,\n\n  DangerZone: ({ id }) => <DeleteResource type=\"Alerter\" id={id} />,\n\n  ResourcePageHeader: ({ id }) => {\n    const alerter = useAlerter(id);\n    return (\n      <ResourcePageHeader\n        intent=\"None\"\n        icon={<AlarmClock className=\"w-8\" />}\n        type=\"Alerter\"\n        id={id}\n        resource={alerter}\n        state={alerter?.info.enabled ? \"Enabled\" : \"Disabled\"}\n        status={alerter?.info.endpoint_type}\n      />\n    );\n  },\n};\n"
  },
  {
    "path": "frontend/src/components/resources/alerter/table.tsx",
    "content": "import { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { ResourceLink } from \"../common\";\nimport { TableTags } from \"@components/tags\";\nimport { Types } from \"komodo_client\";\nimport { useSelectedResources } from \"@lib/hooks\";\n\nexport const AlerterTable = ({\n  alerters,\n}: {\n  alerters: Types.AlerterListItem[];\n}) => {\n  const [_, setSelectedResources] = useSelectedResources(\"Alerter\");\n  return (\n    <DataTable\n      tableKey=\"alerters\"\n      data={alerters}\n      selectOptions={{\n        selectKey: ({ name }) => name,\n        onSelect: setSelectedResources,\n      }}\n      columns={[\n        {\n          accessorKey: \"name\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Name\" />\n          ),\n          cell: ({ row }) => (\n            <ResourceLink type=\"Alerter\" id={row.original.id} />\n          ),\n        },\n        {\n          accessorKey: \"info.endpoint_type\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Type\" />\n          ),\n        },\n        {\n          accessorKey: \"info.enabled\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Enabled\" />\n          ),\n        },\n        {\n          header: \"Tags\",\n          cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/build/actions.tsx",
    "content": "import { ConfirmButton } from \"@components/util\";\nimport { useExecute, usePermissions, useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Ban, Hammer } from \"lucide-react\";\nimport { useBuilder } from \"../builder\";\n\nexport const RunBuild = ({ id }: { id: string }) => {\n  const { canExecute } = usePermissions({ type: \"Build\", id });\n  const building = useRead(\n    \"GetBuildActionState\",\n    { build: id },\n    { refetchInterval: 5_000 }\n  ).data?.building;\n  const updates = useRead(\"ListUpdates\", {\n    query: {\n      \"target.type\": \"Build\",\n      \"target.id\": id,\n    },\n  }).data;\n  const { mutate: run_mutate, isPending: runPending } = useExecute(\"RunBuild\");\n  const { mutate: cancel_mutate, isPending: cancelPending } =\n    useExecute(\"CancelBuild\");\n  const build = useRead(\"ListBuilds\", {}).data?.find((d) => d.id === id);\n  const builder = useBuilder(build?.info.builder_id);\n  const canCancel = builder?.info.builder_type !== \"Server\";\n\n  // make sure hidden without perms.\n  // not usually necessary, but this button also used in deployment actions.\n  if (!canExecute) return null;\n\n  // updates come in in descending order, so 'find' will find latest update matching operation\n  const latestBuild = updates?.updates.find(\n    (u) => u.operation === Types.Operation.RunBuild\n  );\n  const latestCancel = updates?.updates.find(\n    (u) => u.operation === Types.Operation.CancelBuild\n  );\n  const cancelDisabled =\n    !canCancel ||\n    cancelPending ||\n    (latestCancel && latestBuild\n      ? latestCancel!.start_ts > latestBuild!.start_ts\n      : false);\n\n  if (building) {\n    return (\n      <ConfirmButton\n        title=\"Cancel Build\"\n        variant=\"destructive\"\n        icon={<Ban className=\"h-4 w-4\" />}\n        onClick={() => cancel_mutate({ build: id })}\n        disabled={cancelDisabled}\n      />\n    );\n  } else {\n    return (\n      <ConfirmButton\n        title=\"Build\"\n        icon={<Hammer className=\"h-4 w-4\" />}\n        loading={runPending}\n        onClick={() => run_mutate({ build: id })}\n        disabled={runPending}\n      />\n    );\n  }\n};\n"
  },
  {
    "path": "frontend/src/components/resources/build/chart.tsx",
    "content": "// import {\n//   ColorType,\n//   IChartApi,\n//   ISeriesApi,\n//   Time,\n//   createChart,\n// } from \"lightweight-charts\";\n// import { useEffect, useRef } from \"react\";\n// import { useRead } from \"@lib/hooks\";\n// import {\n//   Card,\n//   CardContent,\n//   CardDescription,\n//   CardHeader,\n//   CardTitle,\n// } from \"@ui/card\";\n// import { Hammer } from \"lucide-react\";\n// import { Link } from \"react-router-dom\";\n// import { convertTsMsToLocalUnixTsInSecs } from \"@lib/utils\";\n\n// export const BuildChart = () => {\n//   const container_ref = useRef<HTMLDivElement>(null);\n//   const line_ref = useRef<IChartApi>();\n//   const series_ref = useRef<ISeriesApi<\"Histogram\">>();\n//   const build_stats = useRead(\"GetBuildMonthlyStats\", {}).data;\n//   const summary = useRead(\"GetBuildsSummary\", {}).data;\n\n//   const handleResize = () =>\n//     line_ref.current?.applyOptions({\n//       width: container_ref.current?.clientWidth,\n//     });\n\n//   useEffect(() => {\n//     if (!build_stats) return;\n//     if (line_ref.current) line_ref.current.remove();\n//     const init = () => {\n//       if (!container_ref.current) return;\n\n//       // INIT LINE\n//       line_ref.current = createChart(container_ref.current, {\n//         width: container_ref.current.clientWidth,\n//         height: container_ref.current.clientHeight,\n//         layout: {\n//           background: { type: ColorType.Solid, color: \"transparent\" },\n//           textColor: \"grey\",\n//           fontSize: 12,\n//         },\n//         grid: {\n//           horzLines: { color: \"transparent\" },\n//           vertLines: { color: \"transparent\" },\n//         },\n//         handleScale: false,\n//         handleScroll: false,\n//       });\n//       line_ref.current.timeScale().fitContent();\n\n//       // INIT SERIES\n//       series_ref.current = line_ref.current.addHistogramSeries({\n//         priceLineVisible: false,\n//       });\n//       const max = build_stats.days.reduce((m, c) => Math.max(m, c.time), 0);\n//       series_ref.current.setData(\n//         build_stats.days.map((d) => ({\n//           time: convertTsMsToLocalUnixTsInSecs(d.ts) as Time,\n//           value: d.count,\n//           color:\n//             d.time > max * 0.7\n//               ? \"darkred\"\n//               : d.time > max * 0.35\n//               ? \"darkorange\"\n//               : \"darkgreen\",\n//         })) ?? []\n//       );\n//     };\n\n//     // Run the effect\n//     init();\n//     window.addEventListener(\"resize\", handleResize);\n//     return () => {\n//       window.removeEventListener(\"resize\", handleResize);\n//     };\n//   }, [build_stats]);\n\n//   return (\n//     <Link to=\"/builds\" className=\"w-full\">\n//       <Card className=\"hover:bg-accent/50 transition-colors cursor-pointer\">\n//         <CardHeader>\n//           <div className=\"flex justify-between\">\n//             <div>\n//               <CardTitle>Builds</CardTitle>\n//               <CardDescription className=\"flex gap-2\">\n//                 <div>{summary?.total} Total</div> |{\" \"}\n//                 <div>{build_stats?.total_time.toFixed(2)} Hours</div>\n//               </CardDescription>\n//             </div>\n//             <Hammer className=\"w-4 h-4\" />\n//           </div>\n//         </CardHeader>\n//         <CardContent className=\"hidden xl:flex h-[200px]\">\n//           <div className=\"w-full max-w-full h-full\" ref={container_ref} />\n//         </CardContent>\n//       </Card>\n//     </Link>\n//   );\n// };\n"
  },
  {
    "path": "frontend/src/components/resources/build/config.tsx",
    "content": "import { Config, ConfigComponent } from \"@components/config\";\nimport {\n  AccountSelectorConfig,\n  AddExtraArgMenu,\n  ImageRegistryConfig,\n  ConfigInput,\n  ConfigItem,\n  ConfigList,\n  InputList,\n  ProviderSelectorConfig,\n  SystemCommand,\n  WebhookBuilder,\n} from \"@components/config/util\";\nimport {\n  getWebhookIntegration,\n  useInvalidate,\n  useLocalStorage,\n  usePermissions,\n  useRead,\n  useWebhookIdOrName,\n  useWebhookIntegrations,\n  useWrite,\n} from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Ban, CirclePlus, PlusCircle } from \"lucide-react\";\nimport { ReactNode } from \"react\";\nimport { CopyWebhook, ResourceLink, ResourceSelector } from \"../common\";\nimport { useToast } from \"@ui/use-toast\";\nimport { text_color_class_by_intention } from \"@lib/color\";\nimport { ConfirmButton, ShowHideButton } from \"@components/util\";\nimport { Link } from \"react-router-dom\";\nimport { SecretsSearch } from \"@components/config/env_vars\";\nimport { MonacoEditor } from \"@components/monaco\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { LinkedRepoConfig } from \"@components/config/linked_repo\";\nimport { Button } from \"@ui/button\";\n\ntype BuildMode = \"UI Defined\" | \"Files On Server\" | \"Git Repo\" | undefined;\nconst BUILD_MODES: BuildMode[] = [\"UI Defined\", \"Files On Server\", \"Git Repo\"];\n\nfunction getBuildMode(\n  update: Partial<Types.BuildConfig>,\n  config: Types.BuildConfig\n): BuildMode {\n  if (update.files_on_host ?? config.files_on_host) return \"Files On Server\";\n  if (\n    (update.repo ?? config.repo) ||\n    (update.linked_repo ?? config.linked_repo)\n  )\n    return \"Git Repo\";\n  if (update.dockerfile ?? config.dockerfile) return \"UI Defined\";\n  return undefined;\n}\n\nexport const BuildConfig = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const [show, setShow] = useLocalStorage(`build-${id}-show`, {\n    file: true,\n    git: true,\n    webhooks: true,\n  });\n  const { canWrite } = usePermissions({ type: \"Build\", id });\n  const build = useRead(\"GetBuild\", { build: id }).data;\n  const config = build?.config;\n  const name = build?.name;\n  const webhook = useRead(\"GetBuildWebhookEnabled\", { build: id }).data;\n  const global_disabled =\n    useRead(\"GetCoreInfo\", {}).data?.ui_write_disabled ?? false;\n  const [update, set] = useLocalStorage<Partial<Types.BuildConfig>>(\n    `build-${id}-update-v1`,\n    {}\n  );\n  const { mutateAsync } = useWrite(\"UpdateBuild\");\n  const { integrations } = useWebhookIntegrations();\n  const [id_or_name] = useWebhookIdOrName();\n\n  if (!config) return null;\n\n  const disabled = global_disabled || !canWrite;\n\n  const git_provider = update.git_provider ?? config.git_provider;\n  const webhook_integration = getWebhookIntegration(integrations, git_provider);\n\n  const mode = getBuildMode(update, config);\n\n  const setMode = (mode: BuildMode) => {\n    if (mode === \"Files On Server\") {\n      set({ ...update, files_on_host: true });\n    } else if (mode === \"Git Repo\") {\n      set({\n        ...update,\n        files_on_host: false,\n        repo: update.repo || config.repo || \"namespace/repo\",\n      });\n    } else if (mode === \"UI Defined\") {\n      set({\n        ...update,\n        files_on_host: false,\n        repo: \"\",\n        dockerfile:\n          update.dockerfile ||\n          config.dockerfile ||\n          DEFAULT_BUILD_DOCKERFILE_CONTENTS,\n      });\n    } else if (mode === undefined) {\n      set({\n        ...update,\n        files_on_host: false,\n        repo: \"\",\n        dockerfile: \"\",\n      });\n    }\n  };\n\n  let components: Record<\n    string,\n    false | ConfigComponent<Types.BuildConfig>[] | undefined\n  > = {};\n\n  const builder_component: ConfigComponent<Types.BuildConfig> = {\n    label: \"Builder\",\n    labelHidden: true,\n    components: {\n      builder_id: (builder_id, set) => {\n        return (\n          <ConfigItem\n            label={\n              builder_id ? (\n                <div className=\"flex gap-3 text-lg font-bold\">\n                  Builder:\n                  <ResourceLink type=\"Builder\" id={builder_id} />\n                </div>\n              ) : (\n                \"Select Builder\"\n              )\n            }\n            description=\"Select the Builder to build with.\"\n            boldLabel\n          >\n            <ResourceSelector\n              type=\"Builder\"\n              selected={builder_id}\n              onSelect={(builder_id) => set({ builder_id })}\n              disabled={disabled}\n              align=\"start\"\n            />\n          </ConfigItem>\n        );\n      },\n    },\n  };\n\n  const version_component: ConfigComponent<Types.BuildConfig> = {\n    label: \"Version\",\n    labelHidden: true,\n    components: {\n      version: (_version, set) => {\n        const version =\n          typeof _version === \"object\"\n            ? `${_version.major}.${_version.minor}.${_version.patch}`\n            : _version;\n        return (\n          <ConfigInput\n            className=\"text-lg w-[200px]\"\n            label=\"Version\"\n            boldLabel\n            description=\"Version the image with major.minor.patch. It can be interpolated using [[$VERSION]].\"\n            placeholder=\"0.0.0\"\n            value={version}\n            onChange={(version) => set({ version: version as any })}\n            disabled={disabled}\n          />\n        );\n      },\n      auto_increment_version: {\n        description: \"Automatically increment the patch number on every build.\",\n      },\n    },\n  };\n\n  const choose_mode: ConfigComponent<Types.BuildConfig> = {\n    label: \"Choose Mode\",\n    labelHidden: true,\n    components: {\n      builder_id: () => {\n        return (\n          <ConfigItem\n            label=\"Choose Mode\"\n            description=\"Will the dockerfile contents be defined in UI, stored on the server, or pulled from a git repo?\"\n            boldLabel\n          >\n            <Select\n              value={mode}\n              onValueChange={(mode) => setMode(mode as BuildMode)}\n              disabled={disabled}\n            >\n              <SelectTrigger\n                className=\"w-[200px] capitalize\"\n                disabled={disabled}\n              >\n                <SelectValue placeholder=\"Select Mode\" />\n              </SelectTrigger>\n              <SelectContent>\n                {BUILD_MODES.map((mode) => (\n                  <SelectItem\n                    key={mode}\n                    value={mode!}\n                    className=\"capitalize cursor-pointer\"\n                  >\n                    {mode}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </ConfigItem>\n        );\n      },\n    },\n  };\n\n  const imageName = (update.image_name ?? config.image_name) || name;\n  const customTag = update.image_tag ?? config.image_tag;\n  const customTagPostfix = customTag ? `-${customTag}` : \"\";\n\n  const general_common: ConfigComponent<Types.BuildConfig>[] = [\n    {\n      label: \"Registry\",\n      labelHidden: true,\n      components: {\n        image_registry: (image_registries, set) => (\n          <div className=\"flex flex-col gap-4\">\n            <ConfigItem\n              label=\"Image Registry\"\n              boldLabel\n              description=\"Configure where the built image is pushed.\"\n            >\n              {!disabled && (\n                <Button\n                  variant=\"secondary\"\n                  onClick={() =>\n                    set({\n                      image_registry: [\n                        ...(image_registries ?? []),\n                        { domain: \"\", organization: \"\", account: \"\" },\n                      ],\n                    })\n                  }\n                  className=\"flex items-center gap-2 w-[200px]\"\n                >\n                  <PlusCircle className=\"w-4 h-4\" />\n                  Add Registry\n                </Button>\n              )}\n            </ConfigItem>\n\n            {image_registries?.map((registry, index) => (\n              <ImageRegistryConfig\n                key={\n                  (registry.domain ?? \"\") +\n                  (registry.organization ?? \"\") +\n                  (registry.account ?? \"\") +\n                  index\n                }\n                registry={registry}\n                imageName={imageName}\n                setRegistry={(registry) =>\n                  set({\n                    image_registry:\n                      image_registries?.map((r, i) =>\n                        i === index ? registry : r\n                      ) ?? [],\n                  })\n                }\n                onRemove={() =>\n                  set({\n                    image_registry:\n                      image_registries?.filter((_, i) => i !== index) ?? [],\n                  })\n                }\n                builder_id={update.builder_id ?? config.builder_id}\n                disabled={disabled}\n              />\n            ))}\n          </div>\n        ),\n      },\n    },\n    {\n      label: \"Tagging\",\n      labelHidden: true,\n      components: {\n        image_name: {\n          description: \"Push the image under a different name\",\n          placeholder: \"Custom image name\",\n        },\n        image_tag: {\n          description: `Push a custom tag, plus postfix the other tags (eg ':latest-${customTag ? customTag : \"<TAG>\"}').`,\n          placeholder: \"Custom image tag\",\n        },\n        include_latest_tag: {\n          description: `:latest${customTagPostfix}`,\n        },\n        include_version_tags: {\n          description: `:X.Y.Z${customTagPostfix} + :X.Y${customTagPostfix} + :X${customTagPostfix}`,\n        },\n        include_commit_tag: {\n          description: `:ae8f8ff${customTagPostfix}`,\n        },\n      },\n    },\n    {\n      label: \"Links\",\n      labelHidden: true,\n      components: {\n        links: (values, set) => (\n          <ConfigList\n            label=\"Links\"\n            boldLabel\n            addLabel=\"Add Link\"\n            description=\"Add quick links in the resource header\"\n            field=\"links\"\n            values={values ?? []}\n            set={set}\n            disabled={disabled}\n            placeholder=\"Input link\"\n          />\n        ),\n      },\n    },\n  ];\n\n  const advanced: ConfigComponent<Types.BuildConfig>[] = [\n    {\n      label: \"Pre Build\",\n      description:\n        \"Execute a shell command before running docker build. The 'path' is relative to the root of the repo.\",\n      components: {\n        pre_build: (value, set) => (\n          <SystemCommand\n            value={value}\n            set={(value) => set({ pre_build: value })}\n            disabled={disabled}\n          />\n        ),\n      },\n    },\n    {\n      label: \"Build Args\",\n      description:\n        \"Pass build args to 'docker build'. These can be used in the Dockerfile via ARG, and are visible in the final image.\",\n      labelExtra: !disabled && <SecretsSearch />,\n      components: {\n        build_args: (env, set) => (\n          <MonacoEditor\n            value={env || \"  # VARIABLE = value\\n\"}\n            onValueChange={(build_args) => set({ build_args })}\n            language=\"key_value\"\n            readOnly={disabled}\n          />\n        ),\n      },\n    },\n    {\n      label: \"Secret Args\",\n      description: (\n        <div className=\"flex flex-row flex-wrap gap-2\">\n          <div>\n            Pass secrets to 'docker build'. These values remain hidden in the\n            final image by using docker secret mounts.\n          </div>\n          <Link\n            to=\"https://docs.rs/komodo_client/latest/komodo_client/entities/build/struct.BuildConfig.html#structfield.secret_args\"\n            target=\"_blank\"\n            className=\"text-primary\"\n          >\n            See docker docs.\n          </Link>\n        </div>\n      ),\n      labelExtra: !disabled && <SecretsSearch />,\n      components: {\n        secret_args: (env, set) => (\n          <MonacoEditor\n            value={env || \"  # VARIABLE = value\\n\"}\n            onValueChange={(secret_args) => set({ secret_args })}\n            language=\"key_value\"\n            readOnly={disabled}\n          />\n        ),\n      },\n    },\n    {\n      label: \"Extra Args\",\n      labelHidden: true,\n      components: {\n        extra_args: (value, set) => (\n          <ConfigItem\n            label=\"Extra Args\"\n            boldLabel\n            description={\n              <div className=\"flex flex-row flex-wrap gap-2\">\n                <div>Pass extra arguments to 'docker build'.</div>\n                <Link\n                  to=\"https://docs.docker.com/reference/cli/docker/buildx/build/\"\n                  target=\"_blank\"\n                  className=\"text-primary\"\n                >\n                  See docker docs.\n                </Link>\n              </div>\n            }\n          >\n            {!disabled && (\n              <AddExtraArgMenu\n                type=\"Build\"\n                onSelect={(suggestion) =>\n                  set({\n                    extra_args: [\n                      ...(update.extra_args ?? config.extra_args ?? []),\n                      suggestion,\n                    ],\n                  })\n                }\n                disabled={disabled}\n              />\n            )}\n            <InputList\n              field=\"extra_args\"\n              values={value ?? []}\n              set={set}\n              disabled={disabled}\n              placeholder=\"--extra-arg=value\"\n            />\n          </ConfigItem>\n        ),\n      },\n    },\n    {\n      label: \"Labels\",\n      description: \"Attach --labels to image.\",\n      components: {\n        labels: (labels, set) => (\n          <MonacoEditor\n            value={labels || \"  # your.docker.label: value\\n\"}\n            language=\"key_value\"\n            onValueChange={(labels) => set({ labels })}\n            readOnly={disabled}\n          />\n        ),\n      },\n    },\n  ];\n\n  if (mode === undefined) {\n    components = {\n      \"\": [builder_component, choose_mode],\n    };\n  } else if (mode === \"Files On Server\") {\n    components = {\n      \"\": [\n        builder_component,\n        version_component,\n        {\n          label: \"Files\",\n          components: {\n            build_path: {\n              description: `Set the working directory when running the 'docker build' command. Can be absolute path, or relative to $PERIPHERY_BUILD_DIR/${build.name}`,\n              placeholder: \"/path/to/folder\",\n            },\n            dockerfile_path: {\n              description:\n                \"The path to the dockerfile, relative to the build path.\",\n              placeholder: \"Dockerfile\",\n            },\n          },\n        },\n        ...general_common,\n      ],\n      advanced,\n    };\n  } else if (mode === \"Git Repo\") {\n    const repo_linked = !!(update.linked_repo ?? config.linked_repo);\n    components = {\n      \"\": [\n        builder_component,\n        version_component,\n        {\n          label: \"Source\",\n          contentHidden: !show.git,\n          actions: (\n            <ShowHideButton\n              show={show.git}\n              setShow={(git) => setShow({ ...show, git })}\n            />\n          ),\n          components: {\n            linked_repo: (linked_repo, set) => (\n              <LinkedRepoConfig\n                linked_repo={linked_repo}\n                repo_linked={repo_linked}\n                set={set}\n                disabled={disabled}\n              />\n            ),\n            ...(!repo_linked\n              ? {\n                  git_provider: (provider, set) => {\n                    const https = update.git_https ?? config.git_https;\n                    return (\n                      <ProviderSelectorConfig\n                        account_type=\"git\"\n                        selected={provider}\n                        disabled={disabled}\n                        onSelect={(git_provider) => set({ git_provider })}\n                        https={https}\n                        onHttpsSwitch={() => set({ git_https: !https })}\n                      />\n                    );\n                  },\n                  git_account: (account, set) => (\n                    <AccountSelectorConfig\n                      id={update.builder_id ?? config.builder_id ?? undefined}\n                      type=\"Builder\"\n                      account_type=\"git\"\n                      provider={update.git_provider ?? config.git_provider}\n                      selected={account}\n                      onSelect={(git_account) => set({ git_account })}\n                      disabled={disabled}\n                      placeholder=\"None\"\n                    />\n                  ),\n                  repo: {\n                    placeholder: \"Enter repo\",\n                    description:\n                      \"The repo path on the provider. {namespace}/{repo_name}\",\n                  },\n                  branch: {\n                    placeholder: \"Enter branch\",\n                    description:\n                      \"Select a custom branch, or default to 'main'.\",\n                  },\n                  commit: {\n                    label: \"Commit Hash\",\n                    placeholder: \"Input commit hash\",\n                    description:\n                      \"Optional. Switch to a specific commit hash after cloning the branch.\",\n                  },\n                }\n              : {}),\n          },\n        },\n        {\n          label: \"Files\",\n          components: {\n            build_path: {\n              description: `The directory to run 'docker build', relative to the root of the repo.`,\n              placeholder: \"path/to/folder\",\n            },\n            dockerfile_path: {\n              description:\n                \"The path to the dockerfile, relative to the build path.\",\n              placeholder: \"Dockerfile\",\n            },\n          },\n        },\n        ...general_common,\n        {\n          label: \"Webhook\",\n          description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,\n          contentHidden: !show.webhooks,\n          actions: (\n            <ShowHideButton\n              show={show.webhooks}\n              setShow={(webhooks) => setShow({ ...show, webhooks })}\n            />\n          ),\n          components: {\n            [\"Guard\" as any]: () => {\n              if (update.branch ?? config.branch) {\n                return null;\n              }\n              return (\n                <ConfigItem label=\"Configure Branch\">\n                  <div>Must configure Branch before webhooks will work.</div>\n                </ConfigItem>\n              );\n            },\n            [\"Builder\" as any]: () => (\n              <WebhookBuilder git_provider={git_provider} />\n            ),\n            [\"build\" as any]: () => (\n              <ConfigItem label=\"Webhook Url - Build\">\n                <CopyWebhook\n                  integration={webhook_integration}\n                  path={`/build/${id_or_name === \"Id\" ? id : encodeURIComponent(name ?? \"...\")}`}\n                />\n              </ConfigItem>\n            ),\n            webhook_enabled: webhook !== undefined && !webhook.managed,\n            webhook_secret: {\n              description:\n                \"Provide a custom webhook secret for this resource, or use the global default.\",\n              placeholder: \"Input custom secret\",\n            },\n            [\"managed\" as any]: () => {\n              const inv = useInvalidate();\n              const { toast } = useToast();\n              const { mutate: createWebhook, isPending: createPending } =\n                useWrite(\"CreateBuildWebhook\", {\n                  onSuccess: () => {\n                    toast({ title: \"Webhook Created\" });\n                    inv([\"GetBuildWebhookEnabled\", { build: id }]);\n                  },\n                });\n              const { mutate: deleteWebhook, isPending: deletePending } =\n                useWrite(\"DeleteBuildWebhook\", {\n                  onSuccess: () => {\n                    toast({ title: \"Webhook Deleted\" });\n                    inv([\"GetBuildWebhookEnabled\", { build: id }]);\n                  },\n                });\n              if (!webhook || !webhook.managed) return;\n              return (\n                <ConfigItem label=\"Manage Webhook\">\n                  {webhook.enabled && (\n                    <div className=\"flex items-center gap-4 flex-wrap\">\n                      <div className=\"flex items-center gap-2\">\n                        Incoming webhook is{\" \"}\n                        <div className={text_color_class_by_intention(\"Good\")}>\n                          ENABLED\n                        </div>\n                      </div>\n                      <ConfirmButton\n                        title=\"Disable\"\n                        icon={<Ban className=\"w-4 h-4\" />}\n                        variant=\"destructive\"\n                        onClick={() => deleteWebhook({ build: id })}\n                        loading={deletePending}\n                        disabled={disabled || deletePending}\n                      />\n                    </div>\n                  )}\n                  {!webhook.enabled && (\n                    <div className=\"flex items-center gap-4 flex-wrap\">\n                      <div className=\"flex items-center gap-2\">\n                        Incoming webhook is{\" \"}\n                        <div\n                          className={text_color_class_by_intention(\"Critical\")}\n                        >\n                          DISABLED\n                        </div>\n                      </div>\n                      <ConfirmButton\n                        title=\"Enable Build\"\n                        icon={<CirclePlus className=\"w-4 h-4\" />}\n                        onClick={() => createWebhook({ build: id })}\n                        loading={createPending}\n                        disabled={disabled || createPending}\n                      />\n                    </div>\n                  )}\n                </ConfigItem>\n              );\n            },\n          },\n        },\n      ],\n      advanced,\n    };\n  } else if (mode === \"UI Defined\") {\n    components = {\n      \"\": [\n        builder_component,\n        version_component,\n        {\n          label: \"Dockerfile\",\n          description: \"Manage the dockerfile contents here.\",\n          contentHidden: !show.file,\n          actions: (\n            <ShowHideButton\n              show={show.file}\n              setShow={(file) => setShow({ ...show, file })}\n            />\n          ),\n          components: {\n            dockerfile: (dockerfile, set) => {\n              const show_default =\n                !dockerfile &&\n                update.dockerfile === undefined &&\n                !(update.repo ?? config.repo);\n              return (\n                <div className=\"flex flex-col gap-4\">\n                  <SecretsSearch />\n                  <MonacoEditor\n                    value={\n                      show_default\n                        ? DEFAULT_BUILD_DOCKERFILE_CONTENTS\n                        : dockerfile\n                    }\n                    onValueChange={(dockerfile) => set({ dockerfile })}\n                    language=\"dockerfile\"\n                    readOnly={disabled}\n                  />\n                </div>\n              );\n            },\n          },\n        },\n        ...general_common,\n      ],\n      advanced,\n    };\n  }\n\n  return (\n    <Config\n      titleOther={titleOther}\n      disabled={disabled}\n      original={config}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: update });\n      }}\n      components={components}\n      file_contents_language=\"dockerfile\"\n    />\n  );\n};\n\nexport const DEFAULT_BUILD_DOCKERFILE_CONTENTS = `## Add your dockerfile here\nFROM debian:stable-slim\nRUN echo 'Hello Komodo'\n`;\n"
  },
  {
    "path": "frontend/src/components/resources/build/index.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport {\n  useInvalidate,\n  useLocalStorage,\n  useRead,\n  useUser,\n  useWrite,\n} from \"@lib/hooks\";\nimport { RequiredResourceComponents } from \"@types\";\nimport { Factory, FolderGit, Hammer, Loader2, RefreshCcw } from \"lucide-react\";\nimport { BuildConfig } from \"./config\";\nimport { BuildTable } from \"./table\";\nimport {\n  DeleteResource,\n  NewResource,\n  ResourceLink,\n  ResourcePageHeader,\n  StandardSource,\n} from \"../common\";\nimport { DeploymentTable } from \"../deployment/table\";\nimport { RunBuild } from \"./actions\";\nimport {\n  border_color_class_by_intention,\n  build_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\nimport { cn } from \"@lib/utils\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@ui/tabs\";\nimport { ResourceComponents } from \"..\";\nimport { Types } from \"komodo_client\";\nimport { DashboardPieChart } from \"@pages/home/dashboard\";\nimport { StatusBadge } from \"@components/util\";\nimport { Card } from \"@ui/card\";\nimport { Badge } from \"@ui/badge\";\nimport { useToast } from \"@ui/use-toast\";\nimport { Button } from \"@ui/button\";\nimport { useBuilder } from \"../builder\";\nimport { GroupActions } from \"@components/group-actions\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@ui/tooltip\";\nimport { BuildInfo } from \"./info\";\n\nexport const useBuild = (id?: string) =>\n  useRead(\"ListBuilds\", {}, { refetchInterval: 10_000 }).data?.find(\n    (d) => d.id === id\n  );\n\nexport const useFullBuild = (id: string) =>\n  useRead(\"GetBuild\", { build: id }, { refetchInterval: 10_000 }).data;\n\nconst BuildIcon = ({ id, size }: { id?: string; size: number }) => {\n  const state = useBuild(id)?.info.state;\n  const color = stroke_color_class_by_intention(build_state_intention(state));\n  return <Hammer className={cn(`w-${size} h-${size}`, state && color)} />;\n};\n\nconst ConfigInfoDeployments = ({ id }: { id: string }) => {\n  const [view, setView] = useLocalStorage<\"Config\" | \"Info\" | \"Deployments\">(\n    \"build-tabs-v1\",\n    \"Config\"\n  );\n  const deployments = useRead(\"ListDeployments\", {}).data?.filter(\n    (deployment) => deployment.info.build_id === id\n  );\n  const deploymentsDisabled = (deployments?.length || 0) === 0;\n  const titleOther = (\n    <TabsList className=\"justify-start w-fit\">\n      <TabsTrigger value=\"Config\" className=\"w-[110px]\">\n        Config\n      </TabsTrigger>\n      <TabsTrigger value=\"Info\" className=\"w-[110px]\">\n        Info\n      </TabsTrigger>\n      <TabsTrigger\n        value=\"Deployments\"\n        className=\"w-[110px]\"\n        disabled={deploymentsDisabled}\n      >\n        Deployments\n      </TabsTrigger>\n    </TabsList>\n  );\n  return (\n    <Tabs\n      value={deploymentsDisabled && view === \"Deployments\" ? \"Config\" : view}\n      onValueChange={setView as any}\n    >\n      <TabsContent value=\"Config\">\n        <BuildConfig id={id} titleOther={titleOther} />\n      </TabsContent>\n      <TabsContent value=\"Info\">\n        <BuildInfo id={id} titleOther={titleOther} />\n      </TabsContent>\n      <TabsContent value=\"Deployments\">\n        <Section\n          titleOther={titleOther}\n          actions={<ResourceComponents.Deployment.New build_id={id} />}\n        >\n          <DeploymentTable deployments={deployments ?? []} />\n        </Section>\n      </TabsContent>\n    </Tabs>\n  );\n};\n\nexport const BuildComponents: RequiredResourceComponents = {\n  list_item: (id) => useBuild(id),\n  resource_links: (resource) => (resource.config as Types.BuildConfig).links,\n\n  Description: () => <>Build docker images.</>,\n\n  Dashboard: () => {\n    const summary = useRead(\"GetBuildsSummary\", {}).data;\n    return (\n      <DashboardPieChart\n        data={[\n          { title: \"Ok\", intention: \"Good\", value: summary?.ok ?? 0 },\n          {\n            title: \"Building\",\n            intention: \"Warning\",\n            value: summary?.building ?? 0,\n          },\n          {\n            title: \"Failed\",\n            intention: \"Critical\",\n            value: summary?.failed ?? 0,\n          },\n          {\n            title: \"Unknown\",\n            intention: \"Unknown\",\n            value: summary?.unknown ?? 0,\n          },\n        ]}\n      />\n    );\n  },\n\n  New: () => {\n    const user = useUser().data;\n    const builders = useRead(\"ListBuilders\", {}).data;\n    if (!user) return null;\n    if (!user.admin && !user.create_build_permissions) return null;\n    return (\n      <NewResource\n        type=\"Build\"\n        builder_id={\n          builders && builders.length === 1 ? builders[0].id : undefined\n        }\n      />\n    );\n  },\n\n  GroupActions: () => <GroupActions type=\"Build\" actions={[\"RunBuild\"]} />,\n\n  Table: ({ resources }) => (\n    <BuildTable builds={resources as Types.BuildListItem[]} />\n  ),\n\n  Icon: ({ id }) => <BuildIcon id={id} size={4} />,\n  BigIcon: ({ id }) => <BuildIcon id={id} size={8} />,\n\n  State: ({ id }) => {\n    let state = useBuild(id)?.info.state;\n    return <StatusBadge text={state} intent={build_state_intention(state)} />;\n  },\n\n  Info: {\n    Builder: ({ id }) => {\n      const info = useBuild(id)?.info;\n      const builder = useBuilder(info?.builder_id);\n      return builder?.id ? (\n        <ResourceLink type=\"Builder\" id={builder?.id} />\n      ) : (\n        <div className=\"flex gap-2 items-center text-sm\">\n          <Factory className=\"w-4 h-4\" />\n          <div>Unknown Builder</div>\n        </div>\n      );\n    },\n    Source: ({ id }) => {\n      const info = useBuild(id)?.info;\n      return <StandardSource info={info} />;\n    },\n    Branch: ({ id }) => {\n      const branch = useBuild(id)?.info.branch;\n      return (\n        <div className=\"flex items-center gap-2\">\n          <FolderGit className=\"w-4 h-4\" />\n          {branch}\n        </div>\n      );\n    },\n  },\n\n  Status: {\n    Hash: ({ id }) => {\n      const info = useFullBuild(id)?.info;\n      if (!info?.latest_hash) {\n        return null;\n      }\n      const out_of_date =\n        info.built_hash && info.built_hash !== info.latest_hash;\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card\n              className={cn(\n                \"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer\",\n                out_of_date && border_color_class_by_intention(\"Warning\")\n              )}\n            >\n              <div className=\"text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                {info.built_hash ? \"built\" : \"latest\"}:{\" \"}\n                {info.built_hash || info.latest_hash}\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent>\n            <div className=\"grid gap-2\">\n              <Badge\n                variant=\"secondary\"\n                className=\"w-fit text-muted-foreground\"\n              >\n                message\n              </Badge>\n              {info.built_message || info.latest_message}\n              {out_of_date && (\n                <>\n                  <Badge\n                    variant=\"secondary\"\n                    className={cn(\n                      \"w-fit text-muted-foreground border-[1px]\",\n                      border_color_class_by_intention(\"Warning\")\n                    )}\n                  >\n                    latest\n                  </Badge>\n                  <div>\n                    <span className=\"text-muted-foreground\">\n                      {info.latest_hash}\n                    </span>\n                    : {info.latest_message}\n                  </div>\n                </>\n              )}\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n    Refresh: ({ id }) => {\n      const { toast } = useToast();\n      const inv = useInvalidate();\n      const { mutate, isPending } = useWrite(\"RefreshBuildCache\", {\n        onSuccess: () => {\n          inv([\"ListBuilds\"], [\"GetBuild\", { build: id }]);\n          toast({ title: \"Refreshed build status cache\" });\n        },\n      });\n      return (\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          onClick={() => {\n            mutate({ build: id });\n            toast({ title: \"Triggered refresh of build status cache\" });\n          }}\n        >\n          {isPending ? (\n            <Loader2 className=\"w-4 h-4 animate-spin\" />\n          ) : (\n            <RefreshCcw className=\"w-4 h-4\" />\n          )}\n        </Button>\n      );\n    },\n  },\n\n  Actions: { RunBuild },\n\n  Page: {},\n\n  Config: ConfigInfoDeployments,\n\n  DangerZone: ({ id }) => <DeleteResource type=\"Build\" id={id} />,\n\n  ResourcePageHeader: ({ id }) => {\n    const build = useBuild(id);\n    return (\n      <ResourcePageHeader\n        intent={build_state_intention(build?.info.state)}\n        icon={<BuildIcon id={id} size={8} />}\n        type=\"Build\"\n        id={id}\n        resource={build}\n        state={build?.info.state}\n        status=\"\"\n      />\n    );\n  },\n};\n"
  },
  {
    "path": "frontend/src/components/resources/build/info.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { ReactNode, useState } from \"react\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@ui/card\";\nimport { useFullBuild } from \".\";\nimport { cn, updateLogToHtml } from \"@lib/utils\";\nimport { MonacoEditor } from \"@components/monaco\";\nimport { usePermissions } from \"@lib/hooks\";\nimport { ConfirmUpdate } from \"@components/config/util\";\nimport { useLocalStorage, useRead, useWrite } from \"@lib/hooks\";\nimport { Button } from \"@ui/button\";\nimport { Clock, FilePlus, History } from \"lucide-react\";\nimport { useToast } from \"@ui/use-toast\";\nimport { ConfirmButton, ShowHideButton } from \"@components/util\";\nimport { DEFAULT_BUILD_DOCKERFILE_CONTENTS } from \"./config\";\nimport { fmt_duration } from \"@lib/formatting\";\n\nexport const BuildInfo = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const [edits, setEdits] = useLocalStorage<{ contents: string | undefined }>(\n    `build-${id}-edits`,\n    { contents: undefined }\n  );\n  const [showContents, setShowContents] = useState(true);\n  const { canWrite } = usePermissions({ type: \"Build\", id });\n  const { toast } = useToast();\n  const { mutateAsync, isPending } = useWrite(\"WriteBuildFileContents\", {\n    onSuccess: (res) => {\n      toast({\n        title: res.success ? \"Contents written.\" : \"Failed to write contents.\",\n        variant: res.success ? undefined : \"destructive\",\n      });\n    },\n  });\n\n  const build = useFullBuild(id);\n\n  const recent_builds = useRead(\"ListUpdates\", {\n    query: { \"target.type\": \"Build\", \"target.id\": id, operation: \"RunBuild\" },\n  }).data;\n  const _last_build = recent_builds?.updates[0];\n  const last_build = useRead(\n    \"GetUpdate\",\n    {\n      id: _last_build?.id!,\n    },\n    { enabled: !!_last_build }\n  ).data;\n\n  const file_on_host = build?.config?.files_on_host ?? false;\n  const git_repo =\n    build?.config?.repo || build?.config?.linked_repo ? true : false;\n  const canEdit = canWrite && (file_on_host || git_repo);\n\n  const remote_path = build?.info?.remote_path;\n  const remote_contents = build?.info?.remote_contents;\n  const remote_error = build?.info?.remote_error;\n\n  return (\n    <Section titleOther={titleOther}>\n      {/* Errors */}\n      {remote_error && remote_error.length > 0 && (\n        <Card className=\"flex flex-col gap-4\">\n          <CardHeader className=\"flex flex-row justify-between items-center pb-0\">\n            <div className=\"font-mono flex gap-2\">\n              {remote_path && (\n                <>\n                  <div className=\"text-muted-foreground\">Path:</div>\n                  {remote_path}\n                </>\n              )}\n            </div>\n            {canEdit && (\n              <ConfirmButton\n                title=\"Initialize File\"\n                icon={<FilePlus className=\"w-4 h-4\" />}\n                onClick={() => {\n                  if (build) {\n                    mutateAsync({\n                      build: build.name,\n                      contents: DEFAULT_BUILD_DOCKERFILE_CONTENTS,\n                    });\n                  }\n                }}\n                loading={isPending}\n              />\n            )}\n          </CardHeader>\n          <CardContent className=\"pr-8\">\n            <pre\n              dangerouslySetInnerHTML={{\n                __html: updateLogToHtml(remote_error),\n              }}\n              className=\"max-h-[500px] overflow-y-auto\"\n            />\n          </CardContent>\n        </Card>\n      )}\n\n      {/* Update latest contents */}\n      {remote_contents && remote_contents.length > 0 && (\n        <Card className=\"flex flex-col gap-4\">\n          <CardHeader\n            className={cn(\n              \"flex flex-row justify-between items-center\",\n              showContents && \"pb-0\"\n            )}\n          >\n            {remote_path && (\n              <CardTitle className=\"font-mono flex gap-2\">\n                <div className=\"text-muted-foreground\">Path:</div>\n                {remote_path}\n              </CardTitle>\n            )}\n            <div className=\"flex items-center gap-2\">\n              {canEdit && (\n                <>\n                  <Button\n                    variant=\"outline\"\n                    onClick={() => setEdits({ contents: undefined })}\n                    className=\"flex items-center gap-2\"\n                    disabled={!edits.contents}\n                  >\n                    <History className=\"w-4 h-4\" />\n                    Reset\n                  </Button>\n                  <ConfirmUpdate\n                    previous={{ contents: remote_contents }}\n                    content={{ contents: edits.contents }}\n                    onConfirm={async () => {\n                      if (build) {\n                        return await mutateAsync({\n                          build: build.name,\n                          contents: edits.contents!,\n                        }).then(() => setEdits({ contents: undefined }));\n                      }\n                    }}\n                    disabled={!edits.contents}\n                    language=\"dockerfile\"\n                    loading={isPending}\n                  />\n                </>\n              )}\n              <ShowHideButton show={showContents} setShow={setShowContents} />\n            </div>\n          </CardHeader>\n          {showContents && (\n            <CardContent className=\"pr-8\">\n              <MonacoEditor\n                value={edits.contents ?? remote_contents}\n                language=\"dockerfile\"\n                readOnly={!canEdit}\n                onValueChange={(contents) => setEdits({ contents })}\n              />\n            </CardContent>\n          )}\n        </Card>\n      )}\n\n      {/* Last build output */}\n      {last_build && last_build.logs.length > 0 && (\n        <code className=\"font-bold\">Last Build Logs</code>\n      )}\n      {last_build &&\n        last_build.logs.length > 0 &&\n        last_build.logs?.map((log, i) => (\n          <Card key={i}>\n            <CardHeader className=\"flex-col\">\n              <CardTitle>{log.stage}</CardTitle>\n              <CardDescription className=\"flex gap-2\">\n                <span>\n                  Stage {i + 1} of {last_build.logs.length}\n                </span>\n                <span>|</span>\n                <span className=\"flex items-center gap-2\">\n                  <Clock className=\"w-4 h-4\" />\n                  {fmt_duration(log.start_ts, log.end_ts)}\n                </span>\n              </CardDescription>\n            </CardHeader>\n            <CardContent className=\"flex flex-col gap-2\">\n              {log.command && (\n                <div>\n                  <CardDescription>command</CardDescription>\n                  <pre className=\"max-h-[500px] overflow-y-auto\">\n                    {log.command}\n                  </pre>\n                </div>\n              )}\n              {log.stdout && (\n                <div>\n                  <CardDescription>stdout</CardDescription>\n                  <pre\n                    dangerouslySetInnerHTML={{\n                      __html: updateLogToHtml(log.stdout),\n                    }}\n                    className=\"max-h-[500px] overflow-y-auto\"\n                  />\n                </div>\n              )}\n              {log.stderr && (\n                <div>\n                  <CardDescription>stderr</CardDescription>\n                  <pre\n                    dangerouslySetInnerHTML={{\n                      __html: updateLogToHtml(log.stderr),\n                    }}\n                    className=\"max-h-[500px] overflow-y-auto\"\n                  />\n                </div>\n              )}\n            </CardContent>\n          </Card>\n        ))}\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/build/table.tsx",
    "content": "import { TableTags } from \"@components/tags\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { fmt_version } from \"@lib/formatting\";\nimport { ResourceLink, StandardSource } from \"../common\";\nimport { BuildComponents } from \".\";\nimport { Types } from \"komodo_client\";\nimport { useSelectedResources } from \"@lib/hooks\";\n\nexport const BuildTable = ({ builds }: { builds: Types.BuildListItem[] }) => {\n  const [_, setSelectedResources] = useSelectedResources(\"Build\");\n\n  return (\n    <DataTable\n      tableKey=\"builds\"\n      data={builds}\n      selectOptions={{\n        selectKey: ({ name }) => name,\n        onSelect: setSelectedResources,\n      }}\n      columns={[\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Name\" />\n          ),\n          accessorKey: \"name\",\n          cell: ({ row }) => <ResourceLink type=\"Build\" id={row.original.id} />,\n          size: 200,\n        },\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Source\" />\n          ),\n          accessorKey: \"info.repo\",\n          cell: ({ row }) => <StandardSource info={row.original.info} />,\n          size: 200,\n        },\n        {\n          header: \"Version\",\n          accessorFn: ({ info }) => fmt_version(info.version),\n          size: 120,\n        },\n        {\n          accessorKey: \"info.state\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"State\" />\n          ),\n          cell: ({ row }) => <BuildComponents.State id={row.original.id} />,\n          size: 120,\n        },\n        {\n          header: \"Tags\",\n          cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/builder/config.tsx",
    "content": "import { Config } from \"@components/config\";\nimport { ConfigItem, ConfigList } from \"@components/config/util\";\nimport { useLocalStorage, usePermissions, useRead, useWrite } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { useState } from \"react\";\nimport { ResourceLink, ResourceSelector } from \"../common\";\nimport { Button } from \"@ui/button\";\nimport { MinusCircle, PlusCircle } from \"lucide-react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { Card } from \"@ui/card\";\nimport { cn } from \"@lib/utils\";\nimport { Input } from \"@ui/input\";\nimport { MonacoEditor } from \"@components/monaco\";\n\nexport const BuilderConfig = ({ id }: { id: string }) => {\n  const config = useRead(\"GetBuilder\", { builder: id }).data?.config;\n  if (config?.type === \"Aws\") return <AwsBuilderConfig id={id} />;\n  if (config?.type === \"Server\") return <ServerBuilderConfig id={id} />;\n  if (config?.type === \"Url\") return <UrlBuilderConfig id={id} />;\n};\n\nconst AwsBuilderConfig = ({ id }: { id: string }) => {\n  const { canWrite } = usePermissions({ type: \"Builder\", id });\n  const config = useRead(\"GetBuilder\", { builder: id }).data?.config\n    ?.params as Types.AwsBuilderConfig;\n  const global_disabled =\n    useRead(\"GetCoreInfo\", {}).data?.ui_write_disabled ?? false;\n  const [update, set] = useLocalStorage<Partial<Types.AwsBuilderConfig>>(\n    `aws-builder-${id}-update-v1`,\n    {}\n  );\n  const { mutateAsync } = useWrite(\"UpdateBuilder\");\n  if (!config) return null;\n\n  const disabled = global_disabled || !canWrite;\n\n  return (\n    <Config\n      disabled={disabled}\n      original={config}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: { type: \"Aws\", params: update } });\n      }}\n      components={{\n        \"\": [\n          {\n            label: \"General\",\n            components: {\n              region: {\n                description:\n                  \"Configure the AWS region to launch the instance in.\",\n                placeholder: \"Input region\",\n              },\n              instance_type: {\n                description: \"Choose the instance type to launch\",\n                placeholder: \"Input instance type\",\n              },\n              ami_id: {\n                description:\n                  \"Create an Ami with Docker and Komodo Periphery installed.\",\n                placeholder: \"Input Ami Id\",\n              },\n              volume_gb: {\n                description: \"The size of the disk to attach to the instance.\",\n                placeholder: \"Input size\",\n              },\n              key_pair_name: {\n                description: \"Attach a key pair to the instance\",\n                placeholder: \"Input key pair name\",\n              },\n            },\n          },\n          {\n            label: \"Network\",\n            components: {\n              subnet_id: {\n                description: \"Configure the subnet to launch the instance in.\",\n                placeholder: \"Input subnet id\",\n              },\n              security_group_ids: (values, set) => (\n                <ConfigList\n                  label=\"Security Group Ids\"\n                  description=\"Attach security groups to the instance.\"\n                  field=\"security_group_ids\"\n                  values={values ?? []}\n                  set={set}\n                  disabled={disabled}\n                  placeholder=\"Input Id\"\n                />\n              ),\n              assign_public_ip: {\n                description:\n                  \"Whether to assign a public IP to the build instance.\",\n              },\n              use_public_ip: {\n                description:\n                  \"Whether to connect to the instance over the public IP. Otherwise, will use the internal IP.\",\n              },\n              port: {\n                description: \"Configure the port to connect to Periphery on.\",\n                placeholder: \"Input port\",\n              },\n              use_https: {\n                description: \"Whether to connect to Periphery using HTTPS.\",\n              },\n            },\n          },\n          {\n            label: \"User Data\",\n            description: \"Run a script to setup the instance.\",\n            components: {\n              user_data: (user_data, set) => {\n                return (\n                  <MonacoEditor\n                    value={user_data}\n                    language=\"shell\"\n                    onValueChange={(user_data) => set({ user_data })}\n                    readOnly={disabled}\n                  />\n                );\n              },\n            },\n          },\n        ],\n        additional: [\n          {\n            label: \"Git Providers\",\n            boldLabel: false,\n            description:\n              \"If you configured additional git providers / tokens in Periphery config on the builder, add them here so they will be suggested.\",\n            components: {\n              git_providers: (providers, set) =>\n                providers && (\n                  <>\n                    {!disabled && (\n                      <Button\n                        variant=\"secondary\"\n                        onClick={() =>\n                          set({\n                            git_providers: [\n                              ...(update.git_providers ??\n                                config.git_providers ??\n                                []),\n                              {\n                                domain: \"github.com\",\n                                https: true,\n                                accounts: [],\n                              },\n                            ],\n                          })\n                        }\n                        className=\"flex items-center gap-2 w-[200px]\"\n                      >\n                        <PlusCircle className=\"w-4 h-4\" />\n                        Add Git Provider\n                      </Button>\n                    )}\n                    <ProvidersConfig\n                      type=\"git\"\n                      providers={providers}\n                      set={set}\n                      disabled={disabled}\n                    />\n                  </>\n                ),\n            },\n          },\n          {\n            label: \"Docker Registries\",\n            boldLabel: false,\n            description:\n              \"If you configured additional registries / tokens in Periphery config on the builder, add them here so they will be suggested.\",\n            components: {\n              docker_registries: (providers, set) =>\n                providers && (\n                  <>\n                    {!disabled && (\n                      <Button\n                        variant=\"secondary\"\n                        onClick={() =>\n                          set({\n                            docker_registries: [\n                              ...(update.docker_registries ??\n                                config.docker_registries ??\n                                []),\n                              {\n                                domain: \"docker.io\",\n                                accounts: [],\n                                organizations: [],\n                              },\n                            ],\n                          })\n                        }\n                        className=\"flex items-center gap-2 w-[200px]\"\n                      >\n                        <PlusCircle className=\"w-4 h-4\" />\n                        Add Docker Registry\n                      </Button>\n                    )}\n                    <ProvidersConfig\n                      type=\"docker\"\n                      providers={providers}\n                      set={set}\n                      disabled={disabled}\n                    />\n                  </>\n                ),\n            },\n          },\n          {\n            label: \"Secret Keys\",\n            labelHidden: true,\n            components: {\n              secrets: (secrets, set) => (\n                <ConfigList\n                  label=\"Secret Keys\"\n                  description=\"If you configured additional secrets in Periphery config on the builder, add them here so they will be suggested.\"\n                  field=\"secrets\"\n                  values={secrets ?? []}\n                  set={set}\n                  disabled={disabled}\n                  placeholder=\"SECRET_KEY\"\n                />\n              ),\n            },\n          },\n        ],\n      }}\n    />\n  );\n};\n\nconst ServerBuilderConfig = ({ id }: { id: string }) => {\n  const { canWrite } = usePermissions({ type: \"Builder\", id });\n  const config = useRead(\"GetBuilder\", { builder: id }).data?.config;\n  const [update, set] = useLocalStorage<Partial<Types.ServerBuilderConfig>>(\n    `server-builder-${id}-update-v1`,\n    {}\n  );\n  const { mutateAsync } = useWrite(\"UpdateBuilder\");\n  if (!config) return null;\n\n  const disabled = !canWrite;\n\n  return (\n    <Config\n      disabled={disabled}\n      original={config.params as Types.ServerBuilderConfig}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: { type: \"Server\", params: update } });\n      }}\n      components={{\n        \"\": [\n          {\n            label: \"Server\",\n            labelHidden: true,\n            components: {\n              server_id: (server_id, set) => {\n                return (\n                  <ConfigItem\n                    label={\n                      server_id ? (\n                        <div className=\"flex gap-3 text-lg\">\n                          Server:\n                          <ResourceLink type=\"Server\" id={server_id} />\n                        </div>\n                      ) : (\n                        \"Select Server\"\n                      )\n                    }\n                    description=\"Select the Server to build on.\"\n                  >\n                    <ResourceSelector\n                      type=\"Server\"\n                      selected={server_id}\n                      onSelect={(server_id) => set({ server_id })}\n                      disabled={disabled}\n                      align=\"start\"\n                    />\n                  </ConfigItem>\n                );\n              },\n            },\n          },\n        ],\n      }}\n    />\n  );\n};\n\nconst UrlBuilderConfig = ({ id }: { id: string }) => {\n  const { canWrite } = usePermissions({ type: \"Builder\", id });\n  const config = useRead(\"GetBuilder\", { builder: id }).data?.config;\n  const [update, set] = useLocalStorage<Partial<Types.UrlBuilderConfig>>(\n    `url-builder-${id}-update-v1`,\n    {}\n  );\n  const { mutateAsync } = useWrite(\"UpdateBuilder\");\n  if (!config) return null;\n\n  const disabled = !canWrite;\n\n  return (\n    <Config\n      disabled={disabled}\n      original={config.params as Types.UrlBuilderConfig}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: { type: \"Url\", params: update } });\n      }}\n      components={{\n        \"\": [\n          {\n            label: \"General\",\n            labelHidden: true,\n            components: {\n              address: {\n                description: \"The address of the Periphery agent\",\n                placeholder: \"https://periphery:8120\",\n              },\n              passkey: {\n                description:\n                  \"Use a custom passkey to authenticate with Periphery\",\n                placeholder: \"Custom passkey\",\n              },\n            },\n          },\n        ],\n      }}\n    />\n  );\n};\n\nconst ProvidersConfig = (params: {\n  type: \"git\" | \"docker\";\n  providers: Types.GitProvider[] | Types.DockerRegistry[];\n  set: (input: Partial<Types.AwsBuilderConfig>) => void;\n  disabled: boolean;\n}) => {\n  const arr_field =\n    params.type === \"git\" ? \"git_providers\" : \"docker_registries\";\n  if (!params.providers.length) return null;\n  return (\n    <div className=\"w-full flex\">\n      <div className=\"flex flex-col gap-4 w-full max-w-[400px]\">\n        {params.providers?.map((_, index) => (\n          <div key={index} className=\"flex items-center justify-between gap-4\">\n            <ProviderDialog {...params} index={index} />\n            {!params.disabled && (\n              <Button\n                variant=\"secondary\"\n                onClick={() =>\n                  params.set({\n                    [arr_field]: params.providers.filter((_, i) => i !== index),\n                  })\n                }\n              >\n                <MinusCircle className=\"w-4 h-4\" />\n              </Button>\n            )}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};\n\nconst ProviderDialog = ({\n  type,\n  providers,\n  set,\n  disabled,\n  index,\n}: {\n  type: \"git\" | \"docker\";\n  providers: Types.GitProvider[] | Types.DockerRegistry[];\n  index: number;\n  set: (input: Partial<Types.AwsBuilderConfig>) => void;\n  disabled: boolean;\n}) => {\n  const [open, setOpen] = useState(false);\n  const provider = providers[index];\n  const arr_field = type === \"git\" ? \"git_providers\" : \"docker_registries\";\n  const example_domain = type === \"git\" ? \"github.com\" : \"docker.io\";\n  const update_domain = (domain: string) =>\n    set({\n      [arr_field]: providers.map((provider, i) =>\n        i === index ? { ...provider, domain } : provider\n      ),\n    });\n  const add_account = () =>\n    set({\n      [arr_field]: providers.map(\n        (provider: Types.GitProvider | Types.DockerRegistry, i) =>\n          i === index\n            ? {\n                ...provider,\n                accounts: [...(provider.accounts ?? []), { username: \"\" }],\n              }\n            : provider\n      ) as Types.GitProvider[] | Types.DockerRegistry[],\n    });\n  const update_username = (username: string, account_index: number) =>\n    set({\n      [arr_field]: providers.map(\n        (provider: Types.GitProvider | Types.DockerRegistry, provider_index) =>\n          provider_index === index\n            ? {\n                ...provider,\n                accounts: provider.accounts?.map((account, i) =>\n                  account_index === i ? { username } : account\n                ),\n              }\n            : provider\n      ) as Types.GitProvider[] | Types.DockerRegistry[],\n    });\n  const remove_account = (account_index) =>\n    set({\n      [arr_field]: providers.map(\n        (provider: Types.GitProvider | Types.DockerRegistry, provider_index) =>\n          provider_index === index\n            ? {\n                ...provider,\n                accounts: provider.accounts?.filter(\n                  (_, i) => account_index !== i\n                ),\n              }\n            : provider\n      ) as Types.GitProvider[] | Types.DockerRegistry[],\n    });\n  const add_organization = () =>\n    set({\n      [arr_field]: providers.map((provider: Types.DockerRegistry, i) =>\n        i === index\n          ? {\n              ...provider,\n              organizations: [...(provider.organizations ?? []), \"\"],\n            }\n          : provider\n      ) as Types.DockerRegistry[],\n    });\n  const update_organization = (name: string, organization_index: number) =>\n    set({\n      [arr_field]: providers.map(\n        (provider: Types.DockerRegistry, provider_index) =>\n          provider_index === index\n            ? {\n                ...provider,\n                organizations: provider.organizations?.map((organization, i) =>\n                  organization_index === i ? name : organization\n                ),\n              }\n            : provider\n      ) as Types.GitProvider[] | Types.DockerRegistry[],\n    });\n  const remove_organization = (organization_index) =>\n    set({\n      [arr_field]: providers.map(\n        (provider: Types.DockerRegistry, provider_index) =>\n          provider_index === index\n            ? {\n                ...provider,\n                organizations: provider.organizations?.filter(\n                  (_, i) => organization_index !== i\n                ),\n              }\n            : provider\n      ) as Types.DockerRegistry[],\n    });\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Card className=\"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer w-full\">\n          <div\n            className={cn(\n              \"flex gap-2 text-sm text-nowrap overflow-hidden overflow-ellipsis\"\n            )}\n          >\n            <div className=\"flex gap-2\">{provider.domain}</div>\n            <div className=\"flex gap-2\">\n              <div className=\"text-muted-foreground\">accounts:</div>{\" \"}\n              {provider.accounts?.length || 0}\n            </div>\n            {(provider as Types.DockerRegistry).organizations !== undefined && (\n              <div className=\"flex gap-2\">\n                <div className=\"text-muted-foreground\">organizations:</div>{\" \"}\n                {(provider as Types.DockerRegistry).organizations?.length || 0}\n              </div>\n            )}\n          </div>\n        </Card>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          {type === \"git\" ? \"Git Provider\" : \"Docker Registry\"}\n        </DialogHeader>\n        <div className=\"flex flex-col gap-4\">\n          {/* Domain */}\n          <div className=\"flex items-center justify-between w-fill\">\n            <div className=\"text-nowrap\">Domain</div>\n            <Input\n              value={provider.domain}\n              onChange={(e) => update_domain(e.target.value)}\n              disabled={disabled}\n              className=\"w-[300px]\"\n              placeholder={example_domain}\n            />\n          </div>\n\n          {/* Accounts */}\n          <div className=\"flex flex-col gap-2 w-fill\">\n            <div className=\"flex items-center justify-between w-fill\">\n              <div className=\"text-nowrap\">Available Accounts</div>\n              <Button variant=\"secondary\" onClick={add_account}>\n                Add\n              </Button>\n            </div>\n            <div className=\"flex flex-col gap-2\">\n              {provider.accounts?.map((account, account_index) => {\n                return (\n                  <div\n                    key={account_index}\n                    className=\"flex gap-2 items-center justify-end\"\n                  >\n                    <Input\n                      placeholder=\"Account Username\"\n                      value={account.username}\n                      onChange={(e) =>\n                        update_username(e.target.value, account_index)\n                      }\n                    />\n                    {!disabled && (\n                      <Button\n                        variant=\"secondary\"\n                        onClick={() => remove_account(account_index)}\n                      >\n                        <MinusCircle className=\"w-4 h-4\" />\n                      </Button>\n                    )}\n                  </div>\n                );\n              })}\n            </div>\n          </div>\n\n          {/* Organizations */}\n          {type === \"docker\" && (\n            <div className=\"flex flex-col gap-2 w-fill\">\n              <div className=\"flex items-center justify-between w-fill\">\n                <div className=\"text-nowrap\">Available Organizations</div>\n                <Button variant=\"secondary\" onClick={add_organization}>\n                  Add\n                </Button>\n              </div>\n              <div className=\"flex flex-col gap-2\">\n                {(provider as Types.DockerRegistry).organizations?.map(\n                  (organization, organization_index) => {\n                    return (\n                      <div\n                        key={organization_index}\n                        className=\"flex gap-2 items-center justify-end\"\n                      >\n                        <Input\n                          value={organization}\n                          onChange={(e) =>\n                            update_organization(\n                              e.target.value,\n                              organization_index\n                            )\n                          }\n                          placeholder=\"Organization Name\"\n                        />\n                        {!disabled && (\n                          <Button\n                            variant=\"secondary\"\n                            onClick={() =>\n                              remove_organization(organization_index)\n                            }\n                          >\n                            <MinusCircle className=\"w-4 h-4\" />\n                          </Button>\n                        )}\n                      </div>\n                    );\n                  }\n                )}\n              </div>\n            </div>\n          )}\n        </div>\n        <DialogFooter>\n          <Button onClick={() => setOpen(false)}>Confirm</Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/builder/index.tsx",
    "content": "import { NewLayout } from \"@components/layouts\";\nimport { useRead, useUser, useWrite } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { RequiredResourceComponents } from \"@types\";\nimport { Card, CardDescription, CardHeader, CardTitle } from \"@ui/card\";\nimport { Input } from \"@ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { Cloud, Bot, Factory } from \"lucide-react\";\nimport { ReactNode, useState } from \"react\";\nimport { Link, useNavigate } from \"react-router-dom\";\nimport { BuilderConfig } from \"./config\";\nimport { DeleteResource, ResourceLink, ResourcePageHeader } from \"../common\";\nimport { BuilderTable } from \"./table\";\nimport { GroupActions } from \"@components/group-actions\";\nimport { useServer } from \"../server\";\nimport { cn } from \"@lib/utils\";\nimport {\n  ColorIntention,\n  server_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\n\nexport const useBuilder = (id?: string) =>\n  useRead(\"ListBuilders\", {}, { refetchInterval: 10_000 }).data?.find(\n    (d) => d.id === id\n  );\n\nconst Icon = ({ id, size }: { id?: string; size: number }) => {\n  const info = useBuilder(id)?.info;\n  if (info?.builder_type === \"Server\" && info.instance_type) {\n    return <ServerIcon server_id={info.instance_type} size={size} />;\n  } else {\n    return <Factory className={`w-${size} h-${size}`} />;\n  }\n};\n\nconst ServerIcon = ({\n  server_id,\n  size,\n}: {\n  server_id: string;\n  size: number;\n}) => {\n  const state = useServer(server_id)?.info.state;\n  return (\n    <Factory\n      className={cn(\n        `w-${size} h-${size}`,\n        state && stroke_color_class_by_intention(server_state_intention(state))\n      )}\n    />\n  );\n};\n\nexport const BuilderInstanceType = ({ id }: { id: string }) => {\n  let info = useBuilder(id)?.info;\n  if (info?.builder_type === \"Server\") {\n    return (\n      info.instance_type && (\n        <ResourceLink type=\"Server\" id={info.instance_type} />\n      )\n    );\n  } else {\n    return (\n      <div className=\"flex items-center gap-2\">\n        <Bot className=\"w-4 h-4\" />\n        {info?.instance_type}\n      </div>\n    );\n  }\n};\n\nexport const BuilderComponents: RequiredResourceComponents = {\n  list_item: (id) => useBuilder(id),\n  resource_links: () => undefined,\n\n  Description: () => <>Build on your servers, or single-use AWS instances.</>,\n\n  Dashboard: () => {\n    const builders_count = useRead(\"ListBuilders\", {}).data?.length;\n    return (\n      <Link to=\"/builders/\" className=\"w-full\">\n        <Card className=\"hover:bg-accent/50 transition-colors cursor-pointer\">\n          <CardHeader>\n            <div className=\"flex justify-between\">\n              <div>\n                <CardTitle>Builders</CardTitle>\n                <CardDescription>{builders_count} Total</CardDescription>\n              </div>\n              <Factory className=\"w-4 h-4\" />\n            </div>\n          </CardHeader>\n        </Card>\n      </Link>\n    );\n  },\n\n  New: () => {\n    const is_admin = useUser().data?.admin;\n    const nav = useNavigate();\n    const { mutateAsync } = useWrite(\"CreateBuilder\");\n    const [name, setName] = useState(\"\");\n    const [type, setType] = useState<Types.BuilderConfig[\"type\"]>();\n\n    if (!is_admin) return null;\n\n    return (\n      <NewLayout\n        entityType=\"Builder\"\n        onConfirm={async () => {\n          if (!type) return;\n          const id = (await mutateAsync({ name, config: { type, params: {} } }))\n            ._id?.$oid!;\n          nav(`/builders/${id}`);\n        }}\n        enabled={!!name && !!type}\n      >\n        <div className=\"grid md:grid-cols-2 items-center\">\n          Name\n          <Input\n            placeholder=\"builder-name\"\n            value={name}\n            onChange={(e) => setName(e.target.value)}\n          />\n        </div>\n        <div className=\"grid md:grid-cols-2 items-center\">\n          Builder Type\n          <Select\n            value={type}\n            onValueChange={(value) => setType(value as typeof type)}\n          >\n            <SelectTrigger>\n              <SelectValue placeholder=\"Select Type\" />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectGroup>\n                <SelectItem value=\"Aws\">Aws</SelectItem>\n                <SelectItem value=\"Server\">Server</SelectItem>\n                <SelectItem value=\"Url\">Url</SelectItem>\n              </SelectGroup>\n            </SelectContent>\n          </Select>\n        </div>\n      </NewLayout>\n    );\n  },\n\n  GroupActions: () => <GroupActions type=\"Builder\" actions={[]} />,\n\n  Table: ({ resources }) => (\n    <BuilderTable builders={resources as Types.BuilderListItem[]} />\n  ),\n\n  Icon: ({ id }) => <Icon id={id} size={4} />,\n  BigIcon: ({ id }) => <Icon id={id} size={8} />,\n\n  State: () => null,\n  Status: {},\n\n  Info: {\n    Provider: ({ id }) => {\n      const builder_type = useBuilder(id)?.info.builder_type;\n      return (\n        <div className=\"flex items-center gap-2\">\n          <Cloud className=\"w-4 h-4\" />\n          {builder_type}\n        </div>\n      );\n    },\n    InstanceType: ({ id }) => <BuilderInstanceType id={id} />,\n  },\n\n  Actions: {},\n\n  Page: {},\n\n  Config: BuilderConfig,\n\n  DangerZone: ({ id }) => <DeleteResource type=\"Builder\" id={id} />,\n\n  ResourcePageHeader: ({ id }) => {\n    const builder = useBuilder(id);\n    if (builder?.info.builder_type === \"Server\" && builder.info.instance_type) {\n      return (\n        <ServerInnerResourcePageHeader\n          builder={builder}\n          server_id={builder.info.instance_type}\n        />\n      );\n    }\n    return (\n      <InnerResourcePageHeader\n        id={id}\n        builder={builder}\n        intent=\"None\"\n        icon={<Factory className=\"w-8 h-8\" />}\n      />\n    );\n  },\n};\n\nconst ServerInnerResourcePageHeader = ({\n  builder,\n  server_id,\n}: {\n  builder: Types.BuilderListItem;\n  server_id: string;\n}) => {\n  const state = useServer(server_id)?.info.state;\n  return (\n    <InnerResourcePageHeader\n      id={builder.id}\n      builder={builder}\n      intent={server_state_intention(state)}\n      icon={<ServerIcon server_id={server_id} size={8} />}\n    />\n  );\n};\n\nconst InnerResourcePageHeader = ({\n  id,\n  builder,\n  intent,\n  icon,\n}: {\n  id: string;\n  builder: Types.BuilderListItem | undefined;\n  intent: ColorIntention;\n  icon: ReactNode;\n}) => {\n  return (\n    <ResourcePageHeader\n      intent={intent}\n      icon={icon}\n      type=\"Builder\"\n      id={id}\n      resource={builder}\n      state={builder?.info.builder_type}\n      status={\n        builder?.info.builder_type === \"Aws\"\n          ? builder?.info.instance_type\n          : undefined\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/builder/table.tsx",
    "content": "import { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { ResourceLink } from \"../common\";\nimport { TableTags } from \"@components/tags\";\nimport { BuilderInstanceType } from \".\";\nimport { Types } from \"komodo_client\";\nimport { useSelectedResources } from \"@lib/hooks\";\n\nexport const BuilderTable = ({\n  builders,\n}: {\n  builders: Types.BuilderListItem[];\n}) => {\n  const [_, setSelectedResources] = useSelectedResources(\"Builder\");\n  return (\n    <DataTable\n      tableKey=\"builders\"\n      data={builders}\n      selectOptions={{\n        selectKey: ({ name }) => name,\n        onSelect: setSelectedResources,\n      }}\n      columns={[\n        {\n          accessorKey: \"name\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Name\" />\n          ),\n          cell: ({ row }) => (\n            <ResourceLink type=\"Builder\" id={row.original.id} />\n          ),\n        },\n        {\n          accessorKey: \"info.builder_type\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Provider\" />\n          ),\n        },\n        {\n          accessorKey: \"info.instance_type\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Instance Type\" />\n          ),\n          cell: ({ row }) => <BuilderInstanceType id={row.original.id} />,\n        },\n        {\n          header: \"Tags\",\n          cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/common.tsx",
    "content": "import {\n  ActionWithDialog,\n  ConfirmButton,\n  CopyButton,\n  RepoLink,\n  TemplateMarker,\n  TextUpdateMenuSimple,\n} from \"@components/util\";\nimport {\n  useInvalidate,\n  usePermissions,\n  useRead,\n  useWrite,\n  WebhookIntegration,\n} from \"@lib/hooks\";\nimport { UsableResource } from \"@types\";\nimport { Button } from \"@ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport {\n  Check,\n  ChevronsUpDown,\n  Copy,\n  Edit2,\n  Loader2,\n  NotepadText,\n  SearchX,\n  Server,\n  Trash,\n  X,\n} from \"lucide-react\";\nimport { ReactNode, useEffect, useState } from \"react\";\nimport { Link, useNavigate } from \"react-router-dom\";\nimport { ResourceComponents } from \".\";\nimport { Input } from \"@ui/input\";\nimport { useToast } from \"@ui/use-toast\";\nimport { NewLayout } from \"@components/layouts\";\nimport { Types } from \"komodo_client\";\nimport { cn, filterBySplit, usableResourcePath } from \"@lib/utils\";\nimport {\n  ColorIntention,\n  hex_color_by_intention,\n  text_color_class_by_intention,\n} from \"@lib/color\";\nimport { Switch } from \"@ui/switch\";\nimport { ResourceListItem } from \"komodo_client/dist/types\";\nimport { Badge } from \"@ui/badge\";\n\nexport const ResourcePageHeader = ({\n  type,\n  id,\n  intent,\n  icon,\n  resource,\n  name,\n  state,\n  status,\n}: {\n  type: UsableResource | undefined;\n  id: string | undefined;\n  intent: ColorIntention;\n  icon: ReactNode;\n  resource: Types.ResourceListItem<unknown> | undefined;\n  /** Only pass if not passing resource */\n  name?: string;\n  state: string | undefined;\n  status: string | undefined;\n}) => {\n  const color = text_color_class_by_intention(intent);\n  const background = hex_color_by_intention(intent) + \"15\";\n  return (\n    <div\n      className=\"flex flex-wrap items-center justify-between gap-4 pl-8 pr-8 py-4 rounded-t-md w-full\"\n      style={{ background }}\n    >\n      <div className=\"flex items-center gap-8\">\n        {icon}\n        <div>\n          {type && id && resource?.name ? (\n            <ResourceName type={type} id={id} name={resource.name} />\n          ) : (\n            <p />\n          )}\n          {!type && (\n            <p className=\"text-3xl font-semibold\">{resource?.name ?? name}</p>\n          )}\n          <div className=\"flex items-center gap-2 text-sm uppercase\">\n            <p className={cn(color, \"font-semibold\")}>{state}</p>\n            <p className=\"text-muted-foreground\">{status}</p>\n          </div>\n        </div>\n      </div>\n      {type && id && resource && (\n        <TemplateSwitch type={type} id={id} resource={resource} />\n      )}\n    </div>\n  );\n};\n\nconst TemplateSwitch = ({\n  type,\n  id,\n  resource,\n}: {\n  type: UsableResource;\n  id: string;\n  resource: ResourceListItem<unknown>;\n}) => {\n  const { toast } = useToast();\n  const inv = useInvalidate();\n  const { canWrite } = usePermissions({ type, id });\n  const { mutate, isPending } = useWrite(\"UpdateResourceMeta\", {\n    onSuccess: () => {\n      inv([`List${type}s`], [`Get${type}`]);\n      toast({ title: `Updated is template on ${type} ${resource.name}` });\n    },\n  });\n  return (\n    <div\n      className=\"flex items-center flex-wrap gap-2 cursor-pointer\"\n      onClick={() =>\n        canWrite &&\n        resource &&\n        !isPending &&\n        mutate({ target: { type, id }, template: !resource.template })\n      }\n    >\n      <Badge\n        variant={resource?.template ? \"default\" : \"secondary\"}\n        className=\"text-sm\"\n      >\n        Template\n      </Badge>\n      {isPending ? (\n        <Loader2 className=\"w-4 h-4 animate-spin\" />\n      ) : (\n        <Switch checked={resource?.template} disabled={!canWrite} />\n      )}\n    </div>\n  );\n};\n\nconst ResourceName = ({\n  type,\n  id,\n  name,\n}: {\n  type: UsableResource;\n  id: string;\n  name: string;\n}) => {\n  const invalidate = useInvalidate();\n  const { toast } = useToast();\n  const { canWrite } = usePermissions({ type, id });\n  const [newName, setName] = useState(\"\");\n  const [editing, setEditing] = useState(false);\n  const { mutate, isPending } = useWrite(`Rename${type}`, {\n    onSuccess: () => {\n      invalidate([`List${type}s`]);\n      toast({ title: `${type} Renamed` });\n      setEditing(false);\n    },\n    onError: () => {\n      // If fails, set name back to original\n      setName(name);\n    },\n  });\n  // Ensure the newName is updated if the outer name changes\n  useEffect(() => setName(name), [name]);\n\n  if (editing) {\n    return (\n      <div className=\"flex items-center gap-2\">\n        <Input\n          className=\"text-3xl font-semibold px-1 w-[200px] lg:w-[300px]\"\n          placeholder=\"name\"\n          value={newName}\n          onChange={(e) => setName(e.target.value)}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\") {\n              if (newName && name !== newName) {\n                mutate({ id, name: newName });\n              }\n            } else if (e.key === \"Escape\") {\n              setEditing(false);\n            }\n          }}\n          autoFocus\n        />\n        {name !== newName && (\n          <Button\n            onClick={() => mutate({ id, name: newName })}\n            disabled={!newName || isPending}\n          >\n            {isPending ? <Loader2 className=\"w-4 h-4 animate-spin\" /> : \"Save\"}\n          </Button>\n        )}\n        {name === newName && (\n          <Button variant=\"ghost\" onClick={() => setEditing(false)}>\n            <X className=\"w-4 h-4\" />\n          </Button>\n        )}\n      </div>\n    );\n  } else {\n    return (\n      <div\n        className={cn(\n          \"flex items-center gap-2 w-full\",\n          canWrite && \"cursor-pointer\"\n        )}\n        onClick={() => {\n          if (canWrite) {\n            setEditing(true);\n          }\n        }}\n      >\n        <p className=\"text-3xl font-semibold\">{name}</p>\n        {canWrite && (\n          <Button variant=\"ghost\" className=\"p-2 h-fit\">\n            <Edit2 className=\"w-4 h-4\" />\n          </Button>\n        )}\n      </div>\n    );\n  }\n};\n\nexport const ResourceDescription = ({\n  type,\n  id,\n  disabled,\n}: {\n  type: UsableResource;\n  id: string;\n  disabled: boolean;\n}) => {\n  const { toast } = useToast();\n  const inv = useInvalidate();\n\n  const key = type === \"ResourceSync\" ? \"sync\" : type.toLowerCase();\n\n  const resource = useRead(`Get${type}`, {\n    [key]: id,\n  } as any).data;\n\n  const { mutate: update_description } = useWrite(\"UpdateResourceMeta\", {\n    onSuccess: () => {\n      inv([`Get${type}`]);\n      toast({ title: `Updated description on ${type} ${resource?.name}` });\n    },\n  });\n\n  return (\n    <TextUpdateMenuSimple\n      title=\"Update Description\"\n      placeholder=\"Set Description\"\n      value={resource?.description}\n      onUpdate={(description) =>\n        update_description({\n          target: { type, id },\n          description,\n        })\n      }\n      triggerClassName=\"text-muted-foreground\"\n      disabled={disabled}\n    />\n  );\n};\n\nexport const ResourceSelector = ({\n  type,\n  selected,\n  onSelect,\n  disabled,\n  align,\n  templates = Types.TemplatesQueryBehavior.Exclude,\n  placeholder,\n}: {\n  type: UsableResource;\n  selected: string | undefined;\n  templates?: Types.TemplatesQueryBehavior;\n  onSelect?: (id: string) => void;\n  disabled?: boolean;\n  align?: \"start\" | \"center\" | \"end\";\n  placeholder?: string;\n}) => {\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n\n  const templateFilterFn =\n    templates === Types.TemplatesQueryBehavior.Exclude\n      ? (r: Types.ResourceListItem<unknown>) => !r.template\n      : templates === Types.TemplatesQueryBehavior.Only\n        ? (r: Types.ResourceListItem<unknown>) => r.template\n        : () => true;\n  const resources = useRead(`List${type}s`, {}).data?.filter(templateFilterFn);\n  const name = resources?.find((r) => r.id === selected)?.name;\n\n  if (!resources) return null;\n\n  const filtered = filterBySplit(\n    resources as Types.ResourceListItem<unknown>[],\n    search,\n    (item) => item.name\n  ).sort((a, b) => {\n    if (a.name > b.name) {\n      return 1;\n    } else if (a.name < b.name) {\n      return -1;\n    } else {\n      return 0;\n    }\n  });\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"secondary\"\n          className=\"flex justify-start gap-2 w-fit max-w-[350px]\"\n          disabled={disabled}\n        >\n          {name || (placeholder ?? `Select ${type}`)}\n          {!disabled && <ChevronsUpDown className=\"w-3 h-3\" />}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[300px] max-h-[300px] p-0\" align={align}>\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder={`Search ${type}s`}\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center pt-3 pb-2\">\n              {`No ${type}s Found`}\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n\n            <CommandGroup>\n              {!search && (\n                <CommandItem\n                  onSelect={() => {\n                    onSelect && onSelect(\"\");\n                    setOpen(false);\n                  }}\n                  className=\"flex items-center justify-between cursor-pointer\"\n                >\n                  <div className=\"p-1\">None</div>\n                </CommandItem>\n              )}\n              {filtered.map((resource) => (\n                <CommandItem\n                  key={resource.id}\n                  onSelect={() => {\n                    onSelect && onSelect(resource.id);\n                    setOpen(false);\n                  }}\n                  className=\"flex items-center justify-between cursor-pointer\"\n                >\n                  <div className=\"p-1\">{resource.name}</div>\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nexport const ResourceLink = ({\n  type,\n  id,\n  onClick,\n}: {\n  type: UsableResource;\n  id: string;\n  onClick?: () => void;\n}) => {\n  const Components = ResourceComponents[type];\n  const resource = Components.list_item(id);\n  return (\n    <Link\n      to={`/${usableResourcePath(type)}/${id}`}\n      onClick={(e) => {\n        e.stopPropagation();\n        onClick?.();\n      }}\n      className=\"flex items-center gap-2 text-sm hover:underline\"\n    >\n      <Components.Icon id={id} />\n      <ResourceNameSimple type={type} id={id} />\n      {resource?.template && <TemplateMarker type={type} />}\n    </Link>\n  );\n};\n\nexport const ResourceNameSimple = ({\n  type,\n  id,\n}: {\n  type: UsableResource;\n  id: string;\n}) => {\n  const Components = ResourceComponents[type];\n  const name = Components.list_item(id)?.name ?? \"unknown\";\n  return <>{name}</>;\n};\n\nexport const CopyResource = ({\n  id,\n  disabled,\n  type,\n}: {\n  id: string;\n  disabled?: boolean;\n  type: Exclude<UsableResource, \"Server\">;\n}) => {\n  const [open, setOpen] = useState(false);\n  const [name, setName] = useState(\"\");\n\n  const nav = useNavigate();\n  const inv = useInvalidate();\n  const { mutateAsync: copy } = useWrite(`Copy${type}`);\n\n  const onConfirm = async () => {\n    if (!name) return;\n    try {\n      const res = await copy({ id, name });\n      inv([`List${type}s`]);\n      nav(`/${usableResourcePath(type)}/${res._id?.$oid}`);\n      setOpen(false);\n    } catch (error: any) {\n      // Keep dialog open for validation errors (409/400), close for system errors\n      const status = error?.status || error?.response?.status;\n      if (status !== 409 && status !== 400) {\n        setOpen(false);\n      }\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button\n          variant=\"secondary\"\n          className=\"flex gap-2 items-center\"\n          onClick={() => setOpen(true)}\n          disabled={disabled}\n        >\n          <Copy className=\"w-4 h-4\" />\n          Copy\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Copy {type}</DialogTitle>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-4 my-4\">\n          <p>Provide a name for the newly created {type}.</p>\n          <Input value={name} onChange={(e) => setName(e.target.value)} />\n        </div>\n        <DialogFooter>\n          <ConfirmButton\n            title=\"Copy\"\n            icon={<Check className=\"w-4 h-4\" />}\n            disabled={!name}\n            onClick={async () => {\n              await onConfirm();\n            }}\n          />\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const NewResource = ({\n  type,\n  readable_type,\n  server_id,\n  builder_id,\n  build_id,\n  name: _name = \"\",\n}: {\n  type: UsableResource;\n  readable_type?: string;\n  server_id?: string;\n  builder_id?: string;\n  build_id?: string;\n  name?: string;\n}) => {\n  const nav = useNavigate();\n  const { toast } = useToast();\n  const showTemplateSelector =\n    (useRead(`List${type}s`, {}).data?.filter((r) => r.template).length ?? 0) >\n    0;\n  const { mutateAsync: create } = useWrite(`Create${type}`);\n  const { mutateAsync: copy } = useWrite(`Copy${type}`);\n  const [templateId, setTemplateId] = useState<string>(\"\");\n  const [name, setName] = useState(_name);\n  const type_display =\n    type === \"ResourceSync\" ? \"resource-sync\" : type.toLowerCase();\n  const config: Types._PartialDeploymentConfig | Types._PartialRepoConfig =\n    type === \"Deployment\"\n      ? {\n          server_id,\n          image: build_id\n            ? { type: \"Build\", params: { build_id } }\n            : { type: \"Image\", params: { image: \"\" } },\n        }\n      : type === \"Stack\"\n        ? { server_id }\n        : type === \"Repo\"\n          ? { server_id, builder_id }\n          : type === \"Build\"\n            ? { builder_id }\n            : {};\n  const onConfirm = async () => {\n    if (!name) toast({ title: \"Name cannot be empty\" });\n    const result = templateId\n      ? await copy({ name, id: templateId })\n      : await create({ name, config });\n    const resourceId = result._id?.$oid;\n    if (resourceId) {\n      nav(`/${usableResourcePath(type)}/${resourceId}`);\n    }\n  };\n  return (\n    <NewLayout\n      entityType={readable_type ?? type}\n      onConfirm={onConfirm}\n      enabled={!!name}\n      onOpenChange={() => setName(_name)}\n    >\n      <div className=\"grid md:grid-cols-2 items-center\">\n        {readable_type ?? type} Name\n        <Input\n          placeholder={`${type_display}-name`}\n          value={name}\n          onChange={(e) => setName(e.target.value)}\n          onKeyDown={(e) => {\n            if (!name) return;\n            if (e.key === \"Enter\") {\n              onConfirm().catch(() => {});\n            }\n          }}\n        />\n      </div>\n      {showTemplateSelector && (\n        <div className=\"flex gap-4 justify-between items-center flex-wrap\">\n          Template\n          <ResourceSelector\n            type={type}\n            selected={templateId}\n            onSelect={setTemplateId}\n            templates={Types.TemplatesQueryBehavior.Only}\n            placeholder=\"Select Template\"\n            align=\"end\"\n          />\n        </div>\n      )}\n    </NewLayout>\n  );\n};\n\nexport const DeleteResource = ({\n  type,\n  id,\n}: {\n  type: UsableResource;\n  id: string;\n}) => {\n  const nav = useNavigate();\n  const key = type === \"ResourceSync\" ? \"sync\" : type.toLowerCase();\n  const resource = useRead(`Get${type}`, {\n    [key]: id,\n  } as any).data;\n  const { mutateAsync, isPending } = useWrite(`Delete${type}`);\n\n  if (!resource) return null;\n\n  return (\n    <div className=\"flex items-center justify-end\">\n      <ActionWithDialog\n        name={resource.name}\n        title=\"Delete\"\n        variant=\"destructive\"\n        icon={<Trash className=\"h-4 w-4\" />}\n        onClick={async () => {\n          await mutateAsync({ id });\n          nav(`/${usableResourcePath(type)}`);\n        }}\n        disabled={isPending}\n        loading={isPending}\n        forceConfirmDialog\n      />\n    </div>\n  );\n};\n\nexport const CopyWebhook = ({\n  integration,\n  path,\n}: {\n  integration: WebhookIntegration;\n  path: string;\n}) => {\n  const base_url = useRead(\"GetCoreInfo\", {}).data?.webhook_base_url;\n  const url = base_url + \"/listener/\" + integration.toLowerCase() + path;\n  return (\n    <div className=\"flex gap-2 items-center\">\n      <Input className=\"w-[400px] max-w-[70vw]\" value={url} readOnly />\n      <CopyButton content={url} />\n    </div>\n  );\n};\n\nexport const StandardSource = ({\n  info,\n}: {\n  info:\n    | {\n        linked_repo: string;\n        files_on_host: boolean;\n        repo: string;\n        repo_link: string;\n      }\n    | undefined;\n}) => {\n  if (!info) {\n    return <Loader2 className=\"w-4 h-4 animate-spin\" />;\n  }\n  if (info.files_on_host) {\n    return (\n      <div className=\"flex items-center gap-2\">\n        <Server className=\"w-4 h-4\" />\n        Files on Server\n      </div>\n    );\n  }\n  if (info.linked_repo) {\n    return <ResourceLink type=\"Repo\" id={info.linked_repo} />;\n  }\n  if (info.repo) {\n    return <RepoLink repo={info.repo} link={info.repo_link} />;\n  }\n  return (\n    <div className=\"flex items-center gap-2\">\n      <NotepadText className=\"w-4 h-4\" />\n      UI Defined\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/deployment/actions.tsx",
    "content": "import { ActionWithDialog, ConfirmButton } from \"@components/util\";\nimport {\n  Play,\n  Trash,\n  Pause,\n  Rocket,\n  RefreshCcw,\n  Square,\n  Download,\n} from \"lucide-react\";\nimport { useExecute, useRead } from \"@lib/hooks\";\nimport { useEffect, useState } from \"react\";\nimport { Types } from \"komodo_client\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n} from \"@ui/select\";\nimport { useDeployment } from \".\";\nimport { parse_key_value } from \"@lib/utils\";\n\ninterface DeploymentId {\n  id: string;\n}\n\nexport const DeployDeployment = ({ id }: DeploymentId) => {\n  const deployment = useRead(\"GetDeployment\", { deployment: id }).data;\n  const [signal, setSignal] = useState<Types.TerminationSignal>();\n\n  useEffect(\n    () => setSignal(deployment?.config?.termination_signal),\n    [deployment?.config?.termination_signal]\n  );\n\n  const { mutate: deploy, isPending } = useExecute(\"Deploy\");\n\n  const deployments = useRead(\"ListDeployments\", {}).data;\n  const deployment_item = deployments?.find((d) => d.id === id);\n\n  const deploying = useRead(\n    \"GetDeploymentActionState\",\n    { deployment: id },\n    { refetchInterval: 5_000 }\n  ).data?.deploying;\n\n  const pending = isPending || deploying;\n\n  if (!deployment) return null;\n\n  const deployed =\n    deployment_item?.info.state !== Types.DeploymentState.NotDeployed &&\n    deployment_item?.info.state !== Types.DeploymentState.Unknown;\n\n  const term_signal_labels =\n    deployed &&\n    parse_key_value(deployment.config?.term_signal_labels ?? \"\").map(\n      (s) => ({ signal: s.key, label: s.value }) as Types.TerminationSignalLabel\n    );\n\n  if (deployed) {\n    return (\n      <ActionWithDialog\n        name={deployment.name}\n        title=\"Redeploy\"\n        icon={<Rocket className=\"h-4 w-4\" />}\n        onClick={() => deploy({ deployment: id, stop_signal: signal })}\n        disabled={pending}\n        loading={pending}\n        additional={\n          term_signal_labels && term_signal_labels.length > 1 ? (\n            <TermSignalSelector\n              signals={term_signal_labels}\n              signal={signal}\n              setSignal={setSignal}\n            />\n          ) : undefined\n        }\n      />\n    );\n  } else {\n    return (\n      <ConfirmButton\n        title=\"Deploy\"\n        icon={<Rocket className=\"h-4 w-4\" />}\n        onClick={() => deploy({ deployment: id })}\n        disabled={pending}\n        loading={pending}\n      />\n    );\n  }\n};\n\nexport const DestroyDeployment = ({ id }: DeploymentId) => {\n  const deployment = useRead(\"GetDeployment\", { deployment: id }).data;\n  const [signal, setSignal] = useState<Types.TerminationSignal>();\n\n  useEffect(\n    () => setSignal(deployment?.config?.termination_signal),\n    [deployment?.config?.termination_signal]\n  );\n\n  const { mutate, isPending } = useExecute(\"DestroyDeployment\");\n\n  const deployments = useRead(\"ListDeployments\", {}).data;\n  const state = deployments?.find((d) => d.id === id)?.info.state;\n\n  const destroying = useRead(\n    \"GetDeploymentActionState\",\n    {\n      deployment: id,\n    },\n    { refetchInterval: 5000 }\n  ).data?.destroying;\n\n  const pending = isPending || destroying;\n\n  if (!deployment) return null;\n  if (state === Types.DeploymentState.NotDeployed) return null;\n\n  const term_signal_labels = parse_key_value(\n    deployment.config?.term_signal_labels ?? \"\"\n  ).map(\n    (s) => ({ signal: s.key, label: s.value }) as Types.TerminationSignalLabel\n  );\n\n  return (\n    <ActionWithDialog\n      name={deployment.name}\n      title=\"Destroy\"\n      icon={<Trash className=\"h-4 w-4\" />}\n      onClick={() => mutate({ deployment: id, signal })}\n      disabled={pending}\n      loading={pending}\n      additional={\n        term_signal_labels && term_signal_labels.length > 1 ? (\n          <TermSignalSelector\n            signals={term_signal_labels}\n            signal={signal}\n            setSignal={setSignal}\n          />\n        ) : undefined\n      }\n    />\n  );\n};\n\nexport const PullDeployment = ({ id }: DeploymentId) => {\n  const deployment = useDeployment(id);\n  const { mutate: pull, isPending: pullPending } = useExecute(\"PullDeployment\");\n  const action_state = useRead(\n    \"GetDeploymentActionState\",\n    {\n      deployment: id,\n    },\n    { refetchInterval: 5000 }\n  ).data;\n  if (!deployment) return null;\n\n  return (\n    <ConfirmButton\n      title=\"Pull Image\"\n      icon={<Download className=\"h-4 w-4\" />}\n      onClick={() => pull({ deployment: id })}\n      disabled={pullPending}\n      loading={pullPending || action_state?.pulling}\n    />\n  );\n};\n\nexport const RestartDeployment = ({ id }: DeploymentId) => {\n  const deployment = useDeployment(id);\n  const state = deployment?.info.state;\n  const { mutate: restart, isPending: restartPending } =\n    useExecute(\"RestartDeployment\");\n  const action_state = useRead(\n    \"GetDeploymentActionState\",\n    {\n      deployment: id,\n    },\n    { refetchInterval: 5000 }\n  ).data;\n  if (!deployment) return null;\n\n  if (state !== Types.DeploymentState.Running) {\n    return null;\n  }\n\n  return (\n    <ActionWithDialog\n      name={deployment.name}\n      title=\"Restart\"\n      icon={<RefreshCcw className=\"h-4 w-4\" />}\n      onClick={() => restart({ deployment: id })}\n      disabled={restartPending}\n      loading={restartPending || action_state?.restarting}\n    />\n  );\n};\n\nexport const StartStopDeployment = ({ id }: DeploymentId) => {\n  const deployment = useDeployment(id);\n  const state = deployment?.info.state;\n  const { mutate: start, isPending: startPending } =\n    useExecute(\"StartDeployment\");\n  const action_state = useRead(\n    \"GetDeploymentActionState\",\n    {\n      deployment: id,\n    },\n    { refetchInterval: 5000 }\n  ).data;\n  if (!deployment) return null;\n\n  if (state === Types.DeploymentState.Exited) {\n    return (\n      <ConfirmButton\n        title=\"Start\"\n        icon={<Play className=\"h-4 w-4\" />}\n        onClick={() => start({ deployment: id })}\n        disabled={startPending}\n        loading={startPending || action_state?.starting}\n      />\n    );\n  }\n  if (state !== Types.DeploymentState.NotDeployed) {\n    return <StopDeployment id={id} />;\n  }\n};\n\nconst StopDeployment = ({ id }: DeploymentId) => {\n  const deployment = useRead(\"GetDeployment\", { deployment: id }).data;\n  const [signal, setSignal] = useState<Types.TerminationSignal>();\n\n  useEffect(\n    () => setSignal(deployment?.config?.termination_signal),\n    [deployment?.config?.termination_signal]\n  );\n\n  const { mutate, isPending } = useExecute(\"StopDeployment\");\n  const stopping = useRead(\n    \"GetDeploymentActionState\",\n    {\n      deployment: id,\n    },\n    { refetchInterval: 5000 }\n  ).data?.stopping;\n  const pending = isPending || stopping;\n\n  if (!deployment) return null;\n\n  const term_signal_labels = parse_key_value(\n    deployment.config?.term_signal_labels ?? \"\"\n  ).map(\n    (s) => ({ signal: s.key, label: s.value }) as Types.TerminationSignalLabel\n  );\n\n  return (\n    <ActionWithDialog\n      name={deployment.name}\n      title=\"Stop\"\n      icon={<Square className=\"h-4 w-4\" />}\n      onClick={() => mutate({ deployment: id, signal })}\n      disabled={pending}\n      loading={pending}\n      additional={\n        term_signal_labels && term_signal_labels.length > 1 ? (\n          <TermSignalSelector\n            signals={term_signal_labels}\n            signal={signal}\n            setSignal={setSignal}\n          />\n        ) : undefined\n      }\n    />\n  );\n};\n\nconst TermSignalSelector = ({\n  signals,\n  signal,\n  setSignal,\n}: {\n  signals: Types.TerminationSignalLabel[];\n  signal: Types.TerminationSignal | undefined;\n  setSignal: (signal: Types.TerminationSignal) => void;\n}) => {\n  const label = signals.find((s) => s.signal === signal)?.label;\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <div className=\"text-muted-foreground flex justify-end\">Termination</div>\n      <div className=\"text-muted-foreground flex gap-4 items-center justify-end\">\n        {label}\n        <Select\n          value={signal}\n          onValueChange={(value) => setSignal(value as Types.TerminationSignal)}\n        >\n          <SelectTrigger className=\"w-[200px]\">{signal}</SelectTrigger>\n          <SelectContent>\n            <SelectGroup>\n              {signals.map(({ signal }) => (\n                <SelectItem\n                  key={signal}\n                  value={signal}\n                  className=\"cursor-pointer\"\n                >\n                  {signal}\n                </SelectItem>\n              ))}\n            </SelectGroup>\n          </SelectContent>\n        </Select>\n      </div>\n    </div>\n  );\n};\n\nexport const PauseUnpauseDeployment = ({ id }: DeploymentId) => {\n  const deployment = useDeployment(id);\n  const state = deployment?.info.state;\n  const { mutate: unpause, isPending: unpausePending } =\n    useExecute(\"UnpauseDeployment\");\n  const { mutate: pause, isPending: pausePending } =\n    useExecute(\"PauseDeployment\");\n  const action_state = useRead(\n    \"GetDeploymentActionState\",\n    {\n      deployment: id,\n    },\n    { refetchInterval: 5000 }\n  ).data;\n  if (!deployment) return null;\n\n  if (state === Types.DeploymentState.Paused) {\n    return (\n      <ConfirmButton\n        title=\"Unpause\"\n        icon={<Play className=\"h-4 w-4\" />}\n        onClick={() => unpause({ deployment: id })}\n        disabled={unpausePending}\n        loading={unpausePending || action_state?.unpausing}\n      />\n    );\n  }\n  if (state === Types.DeploymentState.Running) {\n    return (\n      <ActionWithDialog\n        name={deployment.name}\n        title=\"Pause\"\n        icon={<Pause className=\"h-4 w-4\" />}\n        onClick={() => pause({ deployment: id })}\n        disabled={pausePending}\n        loading={pausePending || action_state?.pausing}\n      />\n    );\n  }\n};\n"
  },
  {
    "path": "frontend/src/components/resources/deployment/config/components/image.tsx",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { ResourceSelector } from \"@components/resources/common\";\nimport { fmt_date, fmt_version } from \"@lib/formatting\";\nimport { useRead } from \"@lib/hooks\";\nimport { filterBySplit } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { CaretSortIcon } from \"@radix-ui/react-icons\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport { Input } from \"@ui/input\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { SearchX } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst BuildVersionSelector = ({\n  disabled,\n  buildId,\n  selected,\n  onSelect,\n}: {\n  disabled: boolean;\n  buildId: string | undefined;\n  selected: Types.Version | undefined;\n  onSelect: (version: Types.Version) => void;\n}) => {\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const versions = useRead(\n    \"ListBuildVersions\",\n    { build: buildId! },\n    { enabled: !!buildId }\n  ).data;\n  const filtered = filterBySplit(versions, search, (item) =>\n    fmt_version(item.version)\n  );\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild disabled={disabled}>\n        <div className=\"h-full w-[150px] cursor-pointer flex items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\">\n          {selected ? fmt_version(selected) : \"Latest\"}\n          <CaretSortIcon className=\"h-4 w-4 opacity-50\" />\n        </div>\n      </PopoverTrigger>\n      <PopoverContent align=\"end\" className=\"w-[200px] max-h-[200px] p-0\">\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Search Versions\"\n            value={search}\n            onValueChange={setSearch}\n            className=\"h-9\"\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center\">\n              No Versions Found\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n\n            <CommandGroup>\n              <CommandItem\n                className=\"cursor-pointer\"\n                onSelect={() => {\n                  onSelect({ major: 0, minor: 0, patch: 0 });\n                  setOpen(false);\n                }}\n              >\n                <div>Latest</div>\n              </CommandItem>\n              {filtered?.map((v) => {\n                const version = fmt_version(v.version);\n                return (\n                  <CommandItem\n                    key={version}\n                    onSelect={() => {\n                      onSelect(v.version);\n                      setOpen(false);\n                    }}\n                    className=\"flex items-center justify-between cursor-pointer\"\n                  >\n                    <div>{version}</div>\n                    <div className=\"text-muted-foreground\">\n                      {fmt_date(new Date(v.ts))}\n                    </div>\n                  </CommandItem>\n                );\n              })}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nconst ImageTypeSelector = ({\n  selected,\n  onSelect,\n  disabled,\n}: {\n  selected: Types.DeploymentImage[\"type\"] | undefined;\n  onSelect: (type: Types.DeploymentImage[\"type\"]) => void;\n  disabled: boolean;\n}) => (\n  <Select\n    value={selected || undefined}\n    onValueChange={onSelect}\n    disabled={disabled}\n  >\n    <SelectTrigger className=\"max-w-[150px]\" disabled={disabled}>\n      <SelectValue placeholder=\"Select Type\" />\n    </SelectTrigger>\n    <SelectContent>\n      <SelectItem value={\"Image\"}>Image</SelectItem>\n      <SelectItem value={\"Build\"}>Build</SelectItem>\n    </SelectContent>\n  </Select>\n);\n\nexport const ImageConfig = ({\n  image,\n  set,\n  disabled,\n}: {\n  image: Types.DeploymentImage | undefined;\n  set: (input: Partial<Types.DeploymentConfig>) => void;\n  disabled: boolean;\n}) => (\n  <div className=\"flex gap-4 w-full items-center\">\n    <ImageTypeSelector\n      selected={image?.type}\n      disabled={disabled}\n      onSelect={(type) =>\n        set({\n          image: {\n            type: type,\n            params:\n              type === \"Image\"\n                ? { image: \"\" }\n                : ({\n                    build_id: \"\",\n                    version: { major: 0, minor: 0, patch: 0 },\n                  } as any),\n          },\n        })\n      }\n    />\n    {image?.type === \"Build\" && (\n      <>\n        <ResourceSelector\n          type=\"Build\"\n          selected={image.params.build_id}\n          onSelect={(id) =>\n            set({\n              image: {\n                ...image,\n                params: { ...image.params, build_id: id },\n              },\n            })\n          }\n          disabled={disabled}\n        />\n        <BuildVersionSelector\n          buildId={image.params.build_id}\n          selected={image.params.version}\n          onSelect={(version) =>\n            set({\n              image: {\n                ...image,\n                params: {\n                  ...image.params,\n                  version,\n                },\n              },\n            })\n          }\n          disabled={disabled}\n        />\n      </>\n    )}\n    {image?.type === \"Image\" && (\n      <Input\n        value={image.params.image}\n        onChange={(e) =>\n          set({\n            image: {\n              ...image,\n              params: { image: e.target.value },\n            },\n          })\n        }\n        className=\"w-full\"\n        placeholder=\"image name\"\n        disabled={disabled}\n      />\n    )}\n  </div>\n);\n"
  },
  {
    "path": "frontend/src/components/resources/deployment/config/components/network.tsx",
    "content": "import { ConfigItem } from \"@components/config/util\";\nimport { useRead } from \"@lib/hooks\";\nimport { Input } from \"@ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { useState } from \"react\";\n\nexport const NetworkModeSelector = ({\n  server_id,\n  selected,\n  onSelect,\n  disabled,\n}: {\n  server_id: string | undefined;\n  selected: string | undefined;\n  onSelect: (type: string) => void;\n  disabled: boolean;\n}) => {\n  const _networks =\n    useRead(\n      \"ListDockerNetworks\",\n      { server: server_id! },\n      { enabled: !!server_id }\n    )\n      .data?.filter((n) => n.name)\n      .map((network) => network.name) ?? [];\n  const [customMode, setCustomMode] = useState(false);\n\n  const networks =\n    !selected || _networks.includes(selected)\n      ? _networks\n      : [..._networks, selected];\n\n  return (\n    <ConfigItem\n      label=\"Network Mode\"\n      boldLabel\n      description=\"Choose the --network attached to container\"\n    >\n      {customMode ? (\n        <Input\n          placeholder=\"Input custom network name\"\n          value={selected}\n          onChange={(e) => onSelect(e.target.value)}\n          className=\"max-w-[75%] lg:max-w-[400px]\"\n          onBlur={() => setCustomMode(false)}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\") {\n              setCustomMode(false);\n            }\n          }}\n          autoFocus\n        />\n      ) : (\n        <Select\n          value={selected || undefined}\n          onValueChange={(value) => {\n            if (value === \"Custom\") {\n              setCustomMode(true);\n              onSelect(\"\");\n            } else {\n              onSelect(value);\n            }\n          }}\n          disabled={disabled}\n        >\n          <SelectTrigger className=\"w-[200px]\" disabled={disabled}>\n            <SelectValue placeholder=\"Select Type\" />\n          </SelectTrigger>\n          <SelectContent>\n            {networks\n              ?.filter((network) => network)\n              .map((network) => (\n                <SelectItem\n                  key={network}\n                  value={network!}\n                  className=\"cursor-pointer\"\n                >\n                  {network!}\n                </SelectItem>\n              ))}\n            <SelectItem value=\"Custom\" className=\"cursor-pointer\">\n              Custom\n            </SelectItem>\n          </SelectContent>\n        </Select>\n      )}\n    </ConfigItem>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/deployment/config/components/restart.tsx",
    "content": "import { ConfigItem } from \"@components/config/util\";\nimport { Types } from \"komodo_client\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { object_keys } from \"@lib/utils\";\n\nconst format_mode = (m: string) => m.split(\"-\").join(\" \");\n\nexport const RestartModeSelector = ({\n  selected,\n  set,\n  disabled,\n}: {\n  selected: Types.RestartMode | undefined;\n  set: (input: Partial<Types.DeploymentConfig>) => void;\n  disabled: boolean;\n}) => (\n  <ConfigItem\n    label=\"Restart Mode\"\n    boldLabel\n    description=\"Configure the --restart behavior.\"\n  >\n    <Select\n      value={selected || undefined}\n      onValueChange={(restart: Types.RestartMode) => set({ restart })}\n      disabled={disabled}\n    >\n      <SelectTrigger className=\"w-[200px] capitalize\" disabled={disabled}>\n        <SelectValue placeholder=\"Select Type\" />\n      </SelectTrigger>\n      <SelectContent>\n        {object_keys(Types.RestartMode).map((mode) => (\n          <SelectItem\n            key={mode}\n            value={Types.RestartMode[mode]}\n            className=\"capitalize cursor-pointer\"\n          >\n            {mode === \"NoRestart\"\n              ? \"Don't Restart\"\n              : format_mode(Types.RestartMode[mode])}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  </ConfigItem>\n);\n"
  },
  {
    "path": "frontend/src/components/resources/deployment/config/components/term-signal.tsx",
    "content": "import { ConfigItem } from \"@components/config/util\";\nimport { Types } from \"komodo_client\";\nimport { Input } from \"@ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { useToast } from \"@ui/use-toast\";\nimport { useEffect, useState } from \"react\";\n\nexport const DefaultTerminationSignal = ({\n  arg,\n  set,\n  disabled,\n}: {\n  arg?: Types.TerminationSignal;\n  set: (input: Partial<Types.DeploymentConfig>) => void;\n  disabled: boolean;\n}) => {\n  return (\n    <ConfigItem label=\"Default Termination Signal\">\n      <Select\n        value={arg}\n        onValueChange={(value) =>\n          set({ termination_signal: value as Types.TerminationSignal })\n        }\n        disabled={disabled}\n      >\n        <SelectTrigger className=\"w-[200px]\" disabled={disabled}>\n          <SelectValue placeholder=\"Select Type\" />\n        </SelectTrigger>\n        <SelectContent>\n          <SelectGroup>\n            {Object.values(Types.TerminationSignal)\n              .reverse()\n              .map((term_signal) => (\n                <SelectItem\n                  key={term_signal}\n                  value={term_signal}\n                  className=\"cursor-pointer\"\n                >\n                  {term_signal}\n                </SelectItem>\n              ))}\n          </SelectGroup>\n        </SelectContent>\n      </Select>\n    </ConfigItem>\n  );\n};\n\nexport const TerminationTimeout = ({\n  arg,\n  set,\n  disabled,\n}: {\n  arg: number;\n  set: (input: Partial<Types.DeploymentConfig>) => void;\n  disabled: boolean;\n}) => {\n  const { toast } = useToast();\n  const [input, setInput] = useState(arg.toString());\n  useEffect(() => {\n    setInput(arg.toString());\n  }, [arg]);\n  return (\n    <ConfigItem label=\"Termination Timeout\">\n      <div className=\"flex items-center gap-4\">\n        <Input\n          className=\"w-[100px]\"\n          placeholder=\"time in seconds\"\n          value={input}\n          onChange={(e) => setInput(e.target.value)}\n          onBlur={(e) => {\n            const num = Number(e.target.value);\n            if (num || num === 0) {\n              set({ termination_timeout: num });\n            } else {\n              toast({ title: \"Termination timeout must be a number\" });\n              setInput(arg.toString());\n            }\n          }}\n          disabled={disabled}\n        />\n        seconds\n      </div>\n    </ConfigItem>\n  );\n};"
  },
  {
    "path": "frontend/src/components/resources/deployment/config/index.tsx",
    "content": "import { useLocalStorage, usePermissions, useRead, useWrite } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { ReactNode } from \"react\";\nimport {\n  AccountSelectorConfig,\n  AddExtraArgMenu,\n  ConfigItem,\n  ConfigList,\n  ConfigSwitch,\n  InputList,\n} from \"@components/config/util\";\nimport { ImageConfig } from \"./components/image\";\nimport { RestartModeSelector } from \"./components/restart\";\nimport { NetworkModeSelector } from \"./components/network\";\nimport { Config } from \"@components/config\";\nimport { ResourceLink, ResourceSelector } from \"@components/resources/common\";\nimport { Link } from \"react-router-dom\";\nimport { SecretsSearch } from \"@components/config/env_vars\";\nimport { MonacoEditor } from \"@components/monaco\";\nimport {\n  DefaultTerminationSignal,\n  TerminationTimeout,\n} from \"./components/term-signal\";\nimport { extract_registry_domain } from \"@lib/utils\";\n\nexport const DeploymentConfig = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const { canWrite } = usePermissions({ type: \"Deployment\", id });\n  const config = useRead(\"GetDeployment\", { deployment: id }).data?.config;\n  const builds = useRead(\"ListBuilds\", {}).data;\n  const global_disabled =\n    useRead(\"GetCoreInfo\", {}).data?.ui_write_disabled ?? false;\n  const [update, set] = useLocalStorage<Partial<Types.DeploymentConfig>>(\n    `deployment-${id}-update-v1`,\n    {}\n  );\n  const { mutateAsync } = useWrite(\"UpdateDeployment\");\n\n  if (!config) return null;\n\n  const network = update.network ?? config.network;\n  const hide_ports = network === \"host\" || network === \"none\";\n  const auto_update = update.auto_update ?? config.auto_update ?? false;\n\n  const disabled = global_disabled || !canWrite;\n\n  return (\n    <Config\n      titleOther={titleOther}\n      disabled={disabled}\n      original={config}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: update });\n      }}\n      components={{\n        \"\": [\n          {\n            label: \"Server\",\n            labelHidden: true,\n            components: {\n              server_id: (server_id, set) => {\n                return (\n                  <ConfigItem\n                    label={\n                      server_id ? (\n                        <div className=\"flex gap-3 text-lg font-bold\">\n                          Server:\n                          <ResourceLink type=\"Server\" id={server_id} />\n                        </div>\n                      ) : (\n                        \"Select Server\"\n                      )\n                    }\n                    description=\"Select the Server to deploy on.\"\n                  >\n                    <ResourceSelector\n                      type=\"Server\"\n                      selected={server_id}\n                      onSelect={(server_id) => set({ server_id })}\n                      disabled={disabled}\n                      align=\"start\"\n                    />\n                  </ConfigItem>\n                );\n              },\n            },\n          },\n          {\n            label:\n              (update.image ?? config.image)?.type === \"Build\"\n                ? \"Build\"\n                : \"Image\",\n            description:\n              \"Either pass a docker image directly, or choose a Build to deploy\",\n            components: {\n              image: (value, set) => (\n                <ImageConfig image={value} set={set} disabled={disabled} />\n              ),\n              image_registry_account: (account, set) => {\n                const image = update.image ?? config.image;\n                const provider =\n                  image?.type === \"Image\" && image.params.image\n                    ? extract_registry_domain(image.params.image)\n                    : image?.type === \"Build\" && image.params.build_id\n                      ? builds?.find((b) => b.id === image.params.build_id)\n                          ?.info.image_registry_domain\n                      : undefined;\n                return (\n                  <AccountSelectorConfig\n                    id={update.server_id ?? config.server_id ?? undefined}\n                    type=\"Server\"\n                    account_type=\"docker\"\n                    provider={provider ?? \"docker.io\"}\n                    selected={account}\n                    onSelect={(image_registry_account) =>\n                      set({ image_registry_account })\n                    }\n                    disabled={disabled}\n                    placeholder={\n                      image?.type === \"Build\" ? \"Same as Build\" : undefined\n                    }\n                    description={\n                      image?.type === \"Build\"\n                        ? \"Select an alternate account used to log in to the provider\"\n                        : undefined\n                    }\n                  />\n                );\n              },\n              redeploy_on_build: (update.image?.type ?? config.image?.type) ===\n                \"Build\" && {\n                description: \"Automatically redeploy when the image is built.\",\n              },\n            },\n          },\n          {\n            label: \"Network\",\n            labelHidden: true,\n            components: {\n              network: (value, set) => (\n                <NetworkModeSelector\n                  server_id={update.server_id ?? config.server_id}\n                  selected={value}\n                  onSelect={(network) => set({ network })}\n                  disabled={disabled}\n                />\n              ),\n              ports:\n                !hide_ports &&\n                ((ports, set) => (\n                  <ConfigItem\n                    label=\"Ports\"\n                    description=\"Configure port mappings.\"\n                  >\n                    <MonacoEditor\n                      value={ports || \"  # 3000:3000\\n\"}\n                      language=\"key_value\"\n                      onValueChange={(ports) => set({ ports })}\n                      readOnly={disabled}\n                    />\n                  </ConfigItem>\n                )),\n              links: (values, set) => (\n                <ConfigList\n                  label=\"Links\"\n                  description=\"Add quick links in the resource header\"\n                  field=\"links\"\n                  values={values ?? []}\n                  set={set}\n                  disabled={disabled}\n                  placeholder=\"Input link\"\n                />\n              ),\n            },\n          },\n          {\n            label: \"Environment\",\n            description: \"Pass these variables to the container\",\n            components: {\n              environment: (env, set) => (\n                <div className=\"flex flex-col gap-4\">\n                  <SecretsSearch\n                    server={update.server_id ?? config.server_id}\n                  />\n                  <MonacoEditor\n                    value={env || \"  # VARIABLE = value\\n\"}\n                    onValueChange={(environment) => set({ environment })}\n                    language=\"key_value\"\n                    readOnly={disabled}\n                  />\n                </div>\n              ),\n              // skip_secret_interp: true,\n            },\n          },\n          {\n            label: \"Volumes\",\n            description: \"Configure the volume bindings.\",\n            components: {\n              volumes: (volumes, set) => (\n                <MonacoEditor\n                  value={volumes || \"  # volume:/container/path\\n\"}\n                  language=\"key_value\"\n                  onValueChange={(volumes) => set({ volumes })}\n                  readOnly={disabled}\n                />\n              ),\n            },\n          },\n          {\n            label: \"Restart\",\n            labelHidden: true,\n            components: {\n              restart: (value, set) => (\n                <RestartModeSelector\n                  selected={value}\n                  set={set}\n                  disabled={disabled}\n                />\n              ),\n            },\n          },\n          {\n            label: \"Auto Update\",\n            hidden: (update.image ?? config.image)?.type === \"Build\",\n            components: {\n              poll_for_updates: (poll, set) => {\n                return (\n                  <ConfigSwitch\n                    label=\"Poll for Updates\"\n                    description=\"Check for updates to the image on an interval.\"\n                    value={auto_update || poll}\n                    onChange={(poll_for_updates) => set({ poll_for_updates })}\n                    disabled={disabled || auto_update}\n                  />\n                );\n              },\n              auto_update: {\n                description: \"Trigger a redeploy if a newer image is found.\",\n              },\n            },\n          },\n        ],\n        advanced: [\n          {\n            label: \"Command\",\n            labelHidden: true,\n            components: {\n              command: (value, set) => (\n                <ConfigItem\n                  label=\"Command\"\n                  boldLabel\n                  description={\n                    <div className=\"flex flex-row flex-wrap gap-2\">\n                      <div>Replace the CMD, or extend the ENTRYPOINT.</div>\n                      <Link\n                        to=\"https://docs.docker.com/engine/reference/run/#commands-and-arguments\"\n                        target=\"_blank\"\n                        className=\"text-primary\"\n                      >\n                        See docker docs.\n                        {/* <Button variant=\"link\" className=\"p-0\">\n                        </Button> */}\n                      </Link>\n                    </div>\n                  }\n                >\n                  <MonacoEditor\n                    value={value}\n                    language=\"shell\"\n                    onValueChange={(command) => set({ command })}\n                    readOnly={disabled}\n                  />\n                </ConfigItem>\n              ),\n            },\n          },\n          {\n            label: \"Labels\",\n            description: \"Attach --labels to the container.\",\n            components: {\n              labels: (labels, set) => (\n                <MonacoEditor\n                  value={labels || \"  # your.docker.label: value\\n\"}\n                  language=\"key_value\"\n                  onValueChange={(labels) => set({ labels })}\n                  readOnly={disabled}\n                />\n              ),\n            },\n          },\n          {\n            label: \"Extra Args\",\n            labelHidden: true,\n            components: {\n              extra_args: (value, set) => (\n                <ConfigItem\n                  label=\"Extra Args\"\n                  boldLabel\n                  description={\n                    <div className=\"flex flex-row flex-wrap gap-2\">\n                      <div>Pass extra arguments to 'docker run'.</div>\n                      <Link\n                        to=\"https://docs.docker.com/engine/reference/run/#commands-and-arguments\"\n                        target=\"_blank\"\n                        className=\"text-primary\"\n                      >\n                        See docker docs.\n                      </Link>\n                    </div>\n                  }\n                >\n                  {!disabled && (\n                    <AddExtraArgMenu\n                      type=\"Deployment\"\n                      onSelect={(suggestion) =>\n                        set({\n                          extra_args: [\n                            ...(update.extra_args ?? config.extra_args ?? []),\n                            suggestion,\n                          ],\n                        })\n                      }\n                      disabled={disabled}\n                    />\n                  )}\n                  <InputList\n                    field=\"extra_args\"\n                    values={value ?? []}\n                    set={set}\n                    disabled={disabled}\n                    placeholder=\"--extra-arg=value\"\n                  />\n                </ConfigItem>\n              ),\n            },\n          },\n          {\n            label: \"Termination\",\n            description:\n              \"Configure the signals used to 'docker stop' the container. Options are SIGTERM, SIGQUIT, SIGINT, and SIGHUP.\",\n            components: {\n              termination_signal: (value, set) => (\n                <DefaultTerminationSignal\n                  arg={value}\n                  set={set}\n                  disabled={disabled}\n                />\n              ),\n              termination_timeout: (value, set) => (\n                <TerminationTimeout arg={value} set={set} disabled={disabled} />\n              ),\n              term_signal_labels: (value, set) => (\n                <ConfigItem\n                  label=\"Termination Signal Labels\"\n                  description=\"Choose between multiple signals when stopping\"\n                >\n                  <MonacoEditor\n                    value={value || DEFAULT_TERM_SIGNAL_LABELS}\n                    language=\"key_value\"\n                    onValueChange={(term_signal_labels) =>\n                      set({ term_signal_labels })\n                    }\n                    readOnly={disabled}\n                  />\n                </ConfigItem>\n              ),\n            },\n          },\n        ],\n      }}\n    />\n  );\n};\n\nexport const DEFAULT_TERM_SIGNAL_LABELS = `  # SIGTERM: sigterm label\n  # SIGQUIT: sigquit label\n  # SIGINT: sigint label\n  # SIGHUP: sighup label\n`;\n"
  },
  {
    "path": "frontend/src/components/resources/deployment/index.tsx",
    "content": "import { useLocalStorage, useRead } from \"@lib/hooks\";\nimport { ConnectExecQuery, Types } from \"komodo_client\";\nimport { RequiredResourceComponents } from \"@types\";\nimport { CircleArrowUp, HardDrive, Rocket, Server } from \"lucide-react\";\nimport { cn } from \"@lib/utils\";\nimport { useServer } from \"../server\";\nimport {\n  DeployDeployment,\n  StartStopDeployment,\n  DestroyDeployment,\n  RestartDeployment,\n  PauseUnpauseDeployment,\n  PullDeployment,\n} from \"./actions\";\nimport { DeploymentLogs } from \"./log\";\nimport {\n  deployment_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\nimport { DeploymentTable } from \"./table\";\nimport {\n  DeleteResource,\n  NewResource,\n  ResourceLink,\n  ResourcePageHeader,\n} from \"../common\";\nimport { RunBuild } from \"../build/actions\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@ui/tabs\";\nimport { DeploymentConfig } from \"./config\";\nimport { DashboardPieChart } from \"@pages/home/dashboard\";\nimport {\n  ContainerPortsTableView,\n  DockerResourceLink,\n  StatusBadge,\n} from \"@components/util\";\nimport { GroupActions } from \"@components/group-actions\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@ui/tooltip\";\nimport { usePermissions } from \"@lib/hooks\";\nimport { ContainerTerminal } from \"@components/terminal/container\";\nimport { DeploymentInspect } from \"./inspect\";\nimport { useMemo } from \"react\";\n\n// const configOrLog = atomWithStorage(\"config-or-log-v1\", \"Config\");\n\nexport const useDeployment = (id?: string) =>\n  useRead(\"ListDeployments\", {}, { refetchInterval: 10_000 }).data?.find(\n    (d) => d.id === id\n  );\n\nexport const useFullDeployment = (id: string) =>\n  useRead(\"GetDeployment\", { deployment: id }, { refetchInterval: 10_000 })\n    .data;\n\nconst ConfigTabs = ({ id }: { id: string }) => {\n  const deployment = useDeployment(id);\n  if (!deployment) return null;\n  return <ConfigTabsInner deployment={deployment} />;\n};\n\nconst ConfigTabsInner = ({\n  deployment,\n}: {\n  deployment: Types.DeploymentListItem;\n}) => {\n  // const [view, setView] = useAtom(configOrLog);\n  const [_view, setView] = useLocalStorage<\n    \"Config\" | \"Log\" | \"Inspect\" | \"Terminal\"\n  >(\"deployment-tabs-v1\", \"Config\");\n  const { specificLogs, specificInspect, specificTerminal } = usePermissions({\n    type: \"Deployment\",\n    id: deployment.id,\n  });\n  const container_exec_disabled =\n    useServer(deployment.info.server_id)?.info.container_exec_disabled ?? true;\n  const state = deployment.info.state;\n  const logsDisabled =\n    !specificLogs ||\n    state === undefined ||\n    state === Types.DeploymentState.Unknown ||\n    state === Types.DeploymentState.NotDeployed;\n  const inspectDisabled =\n    !specificInspect ||\n    state === undefined ||\n    state === Types.DeploymentState.Unknown ||\n    state === Types.DeploymentState.NotDeployed;\n  const terminalDisabled =\n    !specificTerminal ||\n    container_exec_disabled ||\n    state !== Types.DeploymentState.Running;\n  const view =\n    (logsDisabled && _view === \"Log\") ||\n    (inspectDisabled && _view === \"Inspect\") ||\n    (terminalDisabled && _view === \"Terminal\")\n      ? \"Config\"\n      : _view;\n\n  const tabs = useMemo(\n    () => (\n      <TabsList className=\"justify-start w-fit\">\n        <TabsTrigger value=\"Config\" className=\"w-[110px]\">\n          Config\n        </TabsTrigger>\n        {specificLogs && (\n          <TabsTrigger\n            value=\"Log\"\n            className=\"w-[110px]\"\n            disabled={logsDisabled}\n          >\n            Log\n          </TabsTrigger>\n        )}\n        {specificInspect && (\n          <TabsTrigger\n            value=\"Inspect\"\n            className=\"w-[110px]\"\n            disabled={inspectDisabled}\n          >\n            Inspect\n          </TabsTrigger>\n        )}\n        {specificTerminal && (\n          <TabsTrigger\n            value=\"Terminal\"\n            className=\"w-[110px]\"\n            disabled={terminalDisabled}\n          >\n            Terminal\n          </TabsTrigger>\n        )}\n      </TabsList>\n    ),\n    [\n      specificLogs,\n      logsDisabled,\n      specificInspect,\n      inspectDisabled,\n      specificTerminal,\n      terminalDisabled,\n    ]\n  );\n  const terminalQuery = useMemo(\n    () =>\n      ({\n        type: \"deployment\",\n        query: {\n          deployment: deployment.id,\n          // This is handled inside ContainerTerminal\n          shell: \"\",\n        },\n      }) as ConnectExecQuery,\n    [deployment.id]\n  );\n  return (\n    <Tabs value={view} onValueChange={setView as any}>\n      <TabsContent value=\"Config\">\n        <DeploymentConfig id={deployment.id} titleOther={tabs} />\n      </TabsContent>\n      <TabsContent value=\"Log\">\n        <DeploymentLogs id={deployment.id} titleOther={tabs} />\n      </TabsContent>\n      <TabsContent value=\"Inspect\">\n        <DeploymentInspect id={deployment.id} titleOther={tabs} />\n      </TabsContent>\n      <TabsContent value=\"Terminal\">\n        <ContainerTerminal query={terminalQuery} titleOther={tabs} />\n      </TabsContent>\n    </Tabs>\n  );\n};\n\nconst DeploymentIcon = ({ id, size }: { id?: string; size: number }) => {\n  const state = useDeployment(id)?.info.state;\n  const color = stroke_color_class_by_intention(\n    deployment_state_intention(state)\n  );\n  return <Rocket className={cn(`w-${size} h-${size}`, state && color)} />;\n};\n\nexport const DeploymentComponents: RequiredResourceComponents = {\n  list_item: (id) => useDeployment(id),\n  resource_links: (resource) =>\n    (resource.config as Types.DeploymentConfig).links,\n\n  Description: () => <>Deploy containers on your servers.</>,\n\n  Dashboard: () => {\n    const summary = useRead(\"GetDeploymentsSummary\", {}).data;\n    const all = [\n      summary?.running ?? 0,\n      summary?.stopped ?? 0,\n      summary?.unhealthy ?? 0,\n      summary?.unknown ?? 0,\n    ];\n    const [running, stopped, unhealthy, unknown] = all;\n    return (\n      <DashboardPieChart\n        data={[\n          all.every((item) => item === 0) && {\n            title: \"Not Deployed\",\n            intention: \"Neutral\",\n            value: summary?.not_deployed ?? 0,\n          },\n          { intention: \"Good\", value: running, title: \"Running\" },\n          {\n            title: \"Stopped\",\n            intention: \"Warning\",\n            value: stopped,\n          },\n          {\n            title: \"Unhealthy\",\n            intention: \"Critical\",\n            value: unhealthy,\n          },\n          {\n            title: \"Unknown\",\n            intention: \"Unknown\",\n            value: unknown,\n          },\n        ]}\n      />\n    );\n  },\n\n  New: ({ server_id: _server_id, build_id }) => {\n    const servers = useRead(\"ListServers\", {}).data;\n    const server_id = _server_id\n      ? _server_id\n      : servers && servers.length === 1\n        ? servers[0].id\n        : undefined;\n    return (\n      <NewResource\n        type=\"Deployment\"\n        server_id={server_id}\n        build_id={build_id}\n      />\n    );\n  },\n\n  Table: ({ resources }) => {\n    return (\n      <DeploymentTable deployments={resources as Types.DeploymentListItem[]} />\n    );\n  },\n\n  GroupActions: () => (\n    <GroupActions\n      type=\"Deployment\"\n      actions={[\n        \"PullDeployment\",\n        \"Deploy\",\n        \"RestartDeployment\",\n        \"StopDeployment\",\n        \"DestroyDeployment\",\n      ]}\n    />\n  ),\n\n  Icon: ({ id }) => <DeploymentIcon id={id} size={4} />,\n  BigIcon: ({ id }) => <DeploymentIcon id={id} size={8} />,\n\n  State: ({ id }) => {\n    const state =\n      useDeployment(id)?.info.state ?? Types.DeploymentState.Unknown;\n    return (\n      <StatusBadge text={state} intent={deployment_state_intention(state)} />\n    );\n  },\n\n  Info: {\n    Server: ({ id }) => {\n      const info = useDeployment(id)?.info;\n      const server = useServer(info?.server_id);\n      return server?.id ? (\n        <ResourceLink type=\"Server\" id={server?.id} />\n      ) : (\n        <div className=\"flex gap-2 items-center text-sm\">\n          <Server className=\"w-4 h-4\" />\n          <div>Unknown Server</div>\n        </div>\n      );\n    },\n    Image: ({ id }) => {\n      const config = useFullDeployment(id)?.config;\n      const info = useDeployment(id)?.info;\n      return info?.build_id ? (\n        <ResourceLink type=\"Build\" id={info.build_id} />\n      ) : (\n        <div className=\"flex gap-2 items-center text-sm\">\n          <HardDrive className=\"w-4 h-4\" />\n          <div>\n            {info?.image.startsWith(\"sha256:\")\n              ? (\n                  config?.image as Extract<\n                    Types.DeploymentImage,\n                    { type: \"Image\" }\n                  >\n                )?.params.image\n              : info?.image || \"N/A\"}\n          </div>\n        </div>\n      );\n    },\n    Container: ({ id }) => {\n      const deployment = useDeployment(id);\n      if (\n        !deployment ||\n        [\n          Types.DeploymentState.Unknown,\n          Types.DeploymentState.NotDeployed,\n        ].includes(deployment.info.state)\n      )\n        return null;\n      return (\n        <DockerResourceLink\n          type=\"container\"\n          name={deployment.name}\n          server_id={deployment.info.server_id}\n        />\n      );\n    },\n    Ports: ({ id }) => {\n      const deployment = useDeployment(id);\n      const container = useRead(\n        \"ListDockerContainers\",\n        {\n          server: deployment?.info.server_id!,\n        },\n        { refetchInterval: 10_000, enabled: !!deployment?.info.server_id }\n      ).data?.find((container) => container.name === deployment?.name);\n      if (!container) return null;\n      return (\n        <ContainerPortsTableView\n          ports={container?.ports ?? []}\n          server_id={deployment?.info.server_id}\n        />\n      );\n    },\n  },\n\n  Status: {\n    UpdateAvailable: ({ id }) => <UpdateAvailable id={id} />,\n  },\n\n  Actions: {\n    RunBuild: ({ id }) => {\n      const build_id = useDeployment(id)?.info.build_id;\n      if (!build_id) return null;\n      return <RunBuild id={build_id} />;\n    },\n    DeployDeployment,\n    PullDeployment,\n    RestartDeployment,\n    PauseUnpauseDeployment,\n    StartStopDeployment,\n    DestroyDeployment,\n  },\n\n  Page: {},\n\n  Config: ConfigTabs,\n\n  DangerZone: ({ id }) => <DeleteResource type=\"Deployment\" id={id} />,\n\n  ResourcePageHeader: ({ id }) => {\n    const deployment = useDeployment(id);\n\n    return (\n      <ResourcePageHeader\n        intent={deployment_state_intention(deployment?.info.state)}\n        icon={<DeploymentIcon id={id} size={8} />}\n        type=\"Deployment\"\n        id={id}\n        resource={deployment}\n        state={\n          deployment?.info.state === Types.DeploymentState.NotDeployed\n            ? \"Not Deployed\"\n            : deployment?.info.state\n        }\n        status={deployment?.info.status}\n      />\n    );\n  },\n};\n\nexport const UpdateAvailable = ({\n  id,\n  small,\n}: {\n  id: string;\n  small?: boolean;\n}) => {\n  const info = useDeployment(id)?.info;\n  const state = info?.state ?? Types.DeploymentState.Unknown;\n  if (\n    !info ||\n    !info?.update_available ||\n    [Types.DeploymentState.NotDeployed, Types.DeploymentState.Unknown].includes(\n      state\n    )\n  ) {\n    return null;\n  }\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <div\n          className={cn(\n            \"px-2 py-1 border rounded-md border-blue-400 hover:border-blue-500 opacity-50 hover:opacity-70 transition-colors cursor-pointer flex items-center gap-2\",\n            small ? \"px-2 py-1\" : \"px-3 py-2\"\n          )}\n        >\n          <CircleArrowUp className=\"w-4 h-4\" />\n          {!small && (\n            <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n              Update Available\n            </div>\n          )}\n        </div>\n      </TooltipTrigger>\n      <TooltipContent className=\"w-fit text-sm\">\n        There is a newer image available\n      </TooltipContent>\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/deployment/inspect.tsx",
    "content": "import { usePermissions, useRead } from \"@lib/hooks\";\nimport { ReactNode } from \"react\";\nimport { Types } from \"komodo_client\";\nimport { Section } from \"@components/layouts\";\nimport { InspectContainerView } from \"@components/inspect\";\n\nexport const DeploymentInspect = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const { specific } = usePermissions({ type: \"Deployment\", id });\n  if (!specific.includes(Types.SpecificPermission.Inspect)) {\n    return (\n      <Section titleOther={titleOther}>\n        <div className=\"min-h-[60vh]\">\n          <h1>User does not have permission to inspect this Deployment.</h1>\n        </div>\n      </Section>\n    );\n  }\n  return (\n    <Section titleOther={titleOther}>\n      <DeploymentInspectInner id={id} />\n    </Section>\n  );\n};\n\nconst DeploymentInspectInner = ({ id }: { id: string }) => {\n  const {\n    data: container,\n    error,\n    isPending,\n    isError,\n  } = useRead(\"InspectDeploymentContainer\", {\n    deployment: id,\n  });\n  return (\n    <InspectContainerView\n      container={container}\n      error={error}\n      isPending={isPending}\n      isError={isError}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/deployment/log.tsx",
    "content": "import { useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { ReactNode } from \"react\";\nimport { useDeployment } from \".\";\nimport { Log, LogSection } from \"@components/log\";\n\nexport const DeploymentLogs = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const state = useDeployment(id)?.info.state;\n  if (\n    state === undefined ||\n    state === Types.DeploymentState.Unknown ||\n    state === Types.DeploymentState.NotDeployed\n  ) {\n    return null;\n  }\n  return <DeploymentLogsInner id={id} titleOther={titleOther} />;\n};\n\nconst DeploymentLogsInner = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  return (\n    <LogSection\n      regular_logs={(timestamps, stream, tail, poll) =>\n        NoSearchLogs(id, tail, timestamps, stream, poll)\n      }\n      search_logs={(timestamps, terms, invert, poll) =>\n        SearchLogs(id, terms, invert, timestamps, poll)\n      }\n      titleOther={titleOther}\n    />\n  );\n};\n\nconst NoSearchLogs = (\n  id: string,\n  tail: number,\n  timestamps: boolean,\n  stream: string,\n  poll: boolean\n) => {\n  const { data: log, refetch } = useRead(\n    \"GetDeploymentLog\",\n    {\n      deployment: id,\n      tail,\n      timestamps,\n    },\n    { refetchInterval: poll ? 3000 : false }\n  );\n  return {\n    Log: (\n      <div className=\"relative\">\n        <Log log={log} stream={stream as \"stdout\" | \"stderr\"} />\n      </div>\n    ),\n    refetch,\n    stderr: !!log?.stderr,\n  };\n};\n\nconst SearchLogs = (\n  id: string,\n  terms: string[],\n  invert: boolean,\n  timestamps: boolean,\n  poll: boolean\n) => {\n  const { data: log, refetch } = useRead(\n    \"SearchDeploymentLog\",\n    {\n      deployment: id,\n      terms,\n      combinator: Types.SearchCombinator.And,\n      invert,\n      timestamps,\n    },\n    { refetchInterval: poll ? 10000 : false }\n  );\n  return {\n    Log: (\n      <div className=\"h-full relative\">\n        <Log log={log} stream=\"stdout\" />\n      </div>\n    ),\n    refetch,\n    stderr: !!log?.stderr,\n  };\n};\n"
  },
  {
    "path": "frontend/src/components/resources/deployment/table.tsx",
    "content": "import { TableTags } from \"@components/tags\";\nimport { Types } from \"komodo_client\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { useRead, useSelectedResources } from \"@lib/hooks\";\nimport { ResourceLink } from \"../common\";\nimport { DeploymentComponents, UpdateAvailable } from \".\";\nimport { HardDrive } from \"lucide-react\";\nimport { useCallback } from \"react\";\n\nexport const DeploymentTable = ({\n  deployments,\n}: {\n  deployments: Types.DeploymentListItem[];\n}) => {\n  const servers = useRead(\"ListServers\", {}).data;\n  const serverName = useCallback(\n    (id: string) => servers?.find((server) => server.id === id)?.name,\n    [servers]\n  );\n\n  const [_, setSelectedResources] = useSelectedResources(\"Deployment\");\n\n  return (\n    <DataTable\n      tableKey=\"deployments\"\n      data={deployments}\n      selectOptions={{\n        selectKey: ({ name }) => name,\n        onSelect: setSelectedResources,\n      }}\n      columns={[\n        {\n          accessorKey: \"name\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Name\" />\n          ),\n          cell: ({ row }) => (\n            <div className=\"flex items-center justify-between gap-2\">\n              <ResourceLink type=\"Deployment\" id={row.original.id} />\n              <UpdateAvailable id={row.original.id} small />\n            </div>\n          ),\n          size: 200,\n        },\n        {\n          accessorKey: \"info.image\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Image\" />\n          ),\n          cell: ({\n            row: {\n              original: {\n                info: { build_id, image },\n              },\n            },\n          }) => <Image build_id={build_id} image={image} />,\n          size: 200,\n        },\n        {\n          accessorKey: \"info.server_id\",\n          sortingFn: (a, b) => {\n            const sa = serverName(a.original.info.server_id);\n            const sb = serverName(b.original.info.server_id);\n\n            if (!sa && !sb) return 0;\n            if (!sa) return 1;\n            if (!sb) return -1;\n\n            if (sa > sb) return 1;\n            else if (sa < sb) return -1;\n            else return 0;\n          },\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Server\" />\n          ),\n          cell: ({ row }) => (\n            <ResourceLink type=\"Server\" id={row.original.info.server_id} />\n          ),\n          size: 200,\n        },\n        {\n          accessorKey: \"info.state\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"State\" />\n          ),\n          cell: ({ row }) => (\n            <DeploymentComponents.State id={row.original.id} />\n          ),\n          size: 120,\n        },\n        {\n          header: \"Tags\",\n          cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n        },\n      ]}\n    />\n  );\n};\n\nconst Image = ({\n  build_id,\n  image,\n}: {\n  build_id: string | undefined;\n  image: string;\n}) => {\n  const builds = useRead(\"ListBuilds\", {}).data;\n  if (build_id) {\n    const build = builds?.find((build) => build.id === build_id);\n    if (build) {\n      return <ResourceLink type=\"Build\" id={build_id} />;\n    } else {\n      return undefined;\n    }\n  } else {\n    const [img] = image.split(\":\");\n    return (\n      <div className=\"flex gap-2 items-center\">\n        <HardDrive className=\"w-4 h-4\" />\n        {img}\n      </div>\n    );\n  }\n};\n"
  },
  {
    "path": "frontend/src/components/resources/index.tsx",
    "content": "import { RequiredResourceComponents, UsableResource } from \"@types\";\nimport { AlerterComponents } from \"./alerter\";\nimport { BuildComponents } from \"./build\";\nimport { BuilderComponents } from \"./builder\";\nimport { DeploymentComponents } from \"./deployment\";\nimport { RepoComponents } from \"./repo\";\nimport { ServerComponents } from \"./server\";\nimport { ProcedureComponents } from \"./procedure/index\";\nimport { ResourceSyncComponents } from \"./resource-sync\";\nimport { StackComponents } from \"./stack\";\nimport { ActionComponents } from \"./action\";\n\nexport const ResourceComponents: {\n  [key in UsableResource]: RequiredResourceComponents;\n} = {\n  Server: ServerComponents,\n  Stack: StackComponents,\n  Deployment: DeploymentComponents,\n  Build: BuildComponents,\n  Repo: RepoComponents,\n  Procedure: ProcedureComponents,\n  Action: ActionComponents,\n  ResourceSync: ResourceSyncComponents,\n  Builder: BuilderComponents,\n  Alerter: AlerterComponents,\n};\n"
  },
  {
    "path": "frontend/src/components/resources/procedure/config.tsx",
    "content": "import {\n  useLocalStorage,\n  usePermissions,\n  useRead,\n  useWebhookIdOrName,\n  useWebhookIntegrations,\n  useWrite,\n} from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Config } from \"@components/config\";\nimport { Button } from \"@ui/button\";\nimport {\n  ConfigItem,\n  ConfigSwitch,\n  WebhookBuilder,\n} from \"@components/config/util\";\nimport { Input } from \"@ui/input\";\nimport { useEffect, useState } from \"react\";\nimport { CopyWebhook, ResourceSelector } from \"../common\";\nimport { Switch } from \"@ui/switch\";\nimport {\n  ArrowDown,\n  ArrowUp,\n  ChevronsUpDown,\n  Minus,\n  MinusCircle,\n  Plus,\n  PlusCircle,\n  SearchX,\n  Settings2,\n  CheckCircle,\n} from \"lucide-react\";\nimport { useToast } from \"@ui/use-toast\";\nimport { TextUpdateMenuMonaco, TimezoneSelector } from \"@components/util\";\nimport { Card } from \"@ui/card\";\nimport { filterBySplit, text_to_env } from \"@lib/utils\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport { fmt_upper_camelcase } from \"@lib/formatting\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@ui/dropdown-menu\";\nimport { DotsHorizontalIcon } from \"@radix-ui/react-icons\";\nimport { DataTable } from \"@ui/data-table\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { MonacoEditor } from \"@components/monaco\";\nimport { quote as shellQuote, parse as shellParse } from \"shell-quote\";\n\ntype ExecutionType = Types.Execution[\"type\"];\n\ntype ExecutionConfigComponent<\n  T extends ExecutionType,\n  P = Extract<Types.Execution, { type: T }>[\"params\"],\n> = React.FC<{\n  params: P;\n  setParams: React.Dispatch<React.SetStateAction<P>>;\n  disabled: boolean;\n}>;\n\ntype MinExecutionType = Exclude<\n  ExecutionType,\n  | \"StartContainer\"\n  | \"RestartContainer\"\n  | \"PauseContainer\"\n  | \"UnpauseContainer\"\n  | \"StopContainer\"\n  | \"DestroyContainer\"\n  | \"DeleteNetwork\"\n  | \"DeleteImage\"\n  | \"DeleteVolume\"\n  | \"TestAlerter\"\n>;\n\ntype ExecutionConfigParams<T extends MinExecutionType> = Extract<\n  Types.Execution,\n  { type: T }\n>[\"params\"];\n\ntype ExecutionConfigs = {\n  [ExType in MinExecutionType]: {\n    Component: ExecutionConfigComponent<ExType>;\n    params: ExecutionConfigParams<ExType>;\n  };\n};\n\nconst PROCEDURE_GIT_PROVIDER = \"Procedure\";\n\nconst new_stage = (next_index: number) => ({\n  name: `Stage ${next_index}`,\n  enabled: true,\n  executions: [default_enabled_execution()],\n});\n\nconst default_enabled_execution: () => Types.EnabledExecution = () => ({\n  enabled: true,\n  execution: {\n    type: \"None\",\n    params: {},\n  },\n});\n\nexport const ProcedureConfig = ({ id }: { id: string }) => {\n  const [branch, setBranch] = useState(\"main\");\n  const { canWrite } = usePermissions({ type: \"Procedure\", id });\n  const procedure = useRead(\"GetProcedure\", { procedure: id }).data;\n  const config = procedure?.config;\n  const name = procedure?.name;\n  const global_disabled =\n    useRead(\"GetCoreInfo\", {}).data?.ui_write_disabled ?? false;\n  const [update, set] = useLocalStorage<Partial<Types.ProcedureConfig>>(\n    `procedure-${id}-update-v1`,\n    {},\n  );\n  const { mutateAsync } = useWrite(\"UpdateProcedure\");\n  const { integrations } = useWebhookIntegrations();\n  const [id_or_name] = useWebhookIdOrName();\n\n  if (!config) return null;\n\n  const disabled = global_disabled || !canWrite;\n  const webhook_integration = integrations[PROCEDURE_GIT_PROVIDER] ?? \"Github\";\n  const stages = update.stages || procedure.config?.stages || [];\n\n  const add_stage = () =>\n    set((config) => ({\n      ...config,\n      stages: [...stages, new_stage(stages.length + 1)],\n    }));\n\n  return (\n    <Config\n      disabled={disabled}\n      original={config}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: update });\n      }}\n      components={{\n        \"\": [\n          {\n            label: \"Stages\",\n            description:\n              \"The executions in a stage are all run in parallel. The stages themselves are run sequentially.\",\n            components: {\n              stages: (stages, set) => (\n                <div className=\"flex flex-col gap-4\">\n                  {stages &&\n                    stages.map((stage, index) => (\n                      <Stage\n                        stage={stage}\n                        setStage={(stage) =>\n                          set({\n                            stages: stages.map((s, i) =>\n                              index === i ? stage : s,\n                            ),\n                          })\n                        }\n                        removeStage={() =>\n                          set({\n                            stages: stages.filter((_, i) => index !== i),\n                          })\n                        }\n                        moveUp={\n                          index === 0\n                            ? undefined\n                            : () =>\n                                set({\n                                  stages: stages.map((stage, i) => {\n                                    // Make sure its not the first row\n                                    if (i === index && index !== 0) {\n                                      return stages[index - 1];\n                                    } else if (i === index - 1) {\n                                      // Reverse the entry, moving this row \"Up\"\n                                      return stages[index];\n                                    } else {\n                                      return stage;\n                                    }\n                                  }),\n                                })\n                        }\n                        moveDown={\n                          index === stages.length - 1\n                            ? undefined\n                            : () =>\n                                set({\n                                  stages: stages.map((stage, i) => {\n                                    // The index also cannot be the last index, which cannot be moved down\n                                    if (\n                                      i === index &&\n                                      index !== stages.length - 1\n                                    ) {\n                                      return stages[index + 1];\n                                    } else if (i === index + 1) {\n                                      // Move the row \"Down\"\n                                      return stages[index];\n                                    } else {\n                                      return stage;\n                                    }\n                                  }),\n                                })\n                        }\n                        insertAbove={() =>\n                          set({\n                            stages: [\n                              ...stages.slice(0, index),\n                              new_stage(index + 1),\n                              ...stages.slice(index),\n                            ],\n                          })\n                        }\n                        insertBelow={() =>\n                          set({\n                            stages: [\n                              ...stages.slice(0, index + 1),\n                              new_stage(index + 2),\n                              ...stages.slice(index + 1),\n                            ],\n                          })\n                        }\n                        disabled={disabled}\n                      />\n                    ))}\n                  <Button\n                    variant=\"secondary\"\n                    onClick={add_stage}\n                    className=\"w-fit\"\n                    disabled={disabled}\n                  >\n                    Add Stage\n                  </Button>\n                </div>\n              ),\n            },\n          },\n          {\n            label: \"Alert\",\n            labelHidden: true,\n            components: {\n              failure_alert: {\n                boldLabel: true,\n                description: \"Send an alert any time the Procedure fails\",\n              },\n            },\n          },\n          {\n            label: \"Schedule\",\n            description:\n              \"Configure the Procedure to run at defined times using English or CRON.\",\n            components: {\n              schedule_enabled: (schedule_enabled, set) => (\n                <ConfigSwitch\n                  label=\"Enabled\"\n                  value={\n                    (update.schedule ?? config.schedule)\n                      ? schedule_enabled\n                      : false\n                  }\n                  disabled={disabled || !(update.schedule ?? config.schedule)}\n                  onChange={(schedule_enabled) => set({ schedule_enabled })}\n                />\n              ),\n              schedule_format: (schedule_format, set) => (\n                <ConfigItem\n                  label=\"Format\"\n                  description=\"Choose whether to provide English or CRON schedule expression\"\n                >\n                  <Select\n                    value={schedule_format}\n                    onValueChange={(schedule_format) =>\n                      set({\n                        schedule_format:\n                          schedule_format as Types.ScheduleFormat,\n                      })\n                    }\n                    disabled={disabled}\n                  >\n                    <SelectTrigger className=\"w-[200px]\" disabled={disabled}>\n                      <SelectValue placeholder=\"Select Format\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      {Object.values(Types.ScheduleFormat).map((mode) => (\n                        <SelectItem\n                          key={mode}\n                          value={mode!}\n                          className=\"cursor-pointer\"\n                        >\n                          {mode}\n                        </SelectItem>\n                      ))}\n                    </SelectContent>\n                  </Select>\n                </ConfigItem>\n              ),\n              schedule: {\n                label: \"Expression\",\n                description:\n                  (update.schedule_format ?? config.schedule_format) ===\n                  \"Cron\" ? (\n                    <div className=\"pt-1 flex flex-col gap-1\">\n                      <code>\n                        second - minute - hour - day - month - day-of-week\n                      </code>\n                    </div>\n                  ) : (\n                    <div className=\"pt-1 flex flex-col gap-1\">\n                      <code>Examples:</code>\n                      <code>- Run every day at 4:00 pm</code>\n                      <code>\n                        - Run at 21:00 on the 1st and 15th of the month\n                      </code>\n                      <code>- Every Sunday at midnight</code>\n                    </div>\n                  ),\n                placeholder:\n                  (update.schedule_format ?? config.schedule_format) === \"Cron\"\n                    ? \"0 0 0 ? * SUN\"\n                    : \"Enter English expression\",\n              },\n              schedule_timezone: (timezone, set) => {\n                return (\n                  <ConfigItem\n                    label=\"Timezone\"\n                    description=\"Select specific IANA timezone for schedule expression.\"\n                  >\n                    <TimezoneSelector\n                      timezone={timezone ?? \"\"}\n                      onChange={(schedule_timezone) =>\n                        set({ schedule_timezone })\n                      }\n                      disabled={disabled}\n                    />\n                  </ConfigItem>\n                );\n              },\n              schedule_alert: {\n                description: \"Send an alert when the scheduled run occurs\",\n              },\n            },\n          },\n          {\n            label: \"Webhook\",\n            description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,\n            components: {\n              [\"Builder\" as any]: () => (\n                <WebhookBuilder git_provider={PROCEDURE_GIT_PROVIDER}>\n                  <div className=\"text-nowrap text-muted-foreground text-sm\">\n                    Listen on branch:\n                  </div>\n                  <div className=\"flex items-center gap-3\">\n                    <Input\n                      placeholder=\"Branch\"\n                      value={branch}\n                      onChange={(e) => setBranch(e.target.value)}\n                      className=\"w-[200px]\"\n                      disabled={branch === \"__ANY__\"}\n                    />\n                    <div className=\"flex items-center gap-2\">\n                      <div className=\"text-muted-foreground text-sm\">\n                        No branch check:\n                      </div>\n                      <Switch\n                        checked={branch === \"__ANY__\"}\n                        onCheckedChange={(checked) => {\n                          if (checked) {\n                            setBranch(\"__ANY__\");\n                          } else {\n                            setBranch(\"main\");\n                          }\n                        }}\n                      />\n                    </div>\n                  </div>\n                </WebhookBuilder>\n              ),\n              [\"run\" as any]: () => (\n                <ConfigItem label=\"Webhook Url - Run\">\n                  <CopyWebhook\n                    integration={webhook_integration}\n                    path={`/procedure/${id_or_name === \"Id\" ? id : encodeURIComponent(name ?? \"...\")}/${branch}`}\n                  />\n                </ConfigItem>\n              ),\n              webhook_enabled: true,\n              webhook_secret: {\n                description:\n                  \"Provide a custom webhook secret for this resource, or use the global default.\",\n                placeholder: \"Input custom secret\",\n              },\n            },\n          },\n        ],\n      }}\n    />\n  );\n};\n\nconst Stage = ({\n  stage,\n  setStage,\n  removeStage,\n  moveUp,\n  moveDown,\n  insertAbove,\n  insertBelow,\n  disabled,\n}: {\n  stage: Types.ProcedureStage;\n  setStage: (stage: Types.ProcedureStage) => void;\n  removeStage: () => void;\n  insertAbove: () => void;\n  insertBelow: () => void;\n  moveUp: (() => void) | undefined;\n  moveDown: (() => void) | undefined;\n  disabled: boolean;\n}) => {\n  return (\n    <Card className=\"p-4 flex flex-col gap-4\">\n      <div className=\"flex justify-between items-center\">\n        <Input\n          value={stage.name}\n          onChange={(e) => setStage({ ...stage, name: e.target.value })}\n          className=\"w-[300px] text-md\"\n        />\n        <div className=\"flex gap-4 items-center\">\n          <div>Enabled:</div>\n          <Switch\n            checked={stage.enabled}\n            onCheckedChange={(enabled) => setStage({ ...stage, enabled })}\n          />\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild disabled={disabled}>\n              <Button\n                variant=\"ghost\"\n                className=\"h-8 w-8 p-0\"\n                disabled={disabled}\n              >\n                <span className=\"sr-only\">Open menu</span>\n                <DotsHorizontalIcon className=\"h-4 w-4\" />\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\">\n              {moveUp && (\n                <DropdownMenuItem\n                  className=\"flex gap-4 justify-between cursor-pointer\"\n                  onClick={moveUp}\n                >\n                  Move Up <ArrowUp className=\"w-4 h-4\" />\n                </DropdownMenuItem>\n              )}\n              {moveDown && (\n                <DropdownMenuItem\n                  className=\"flex gap-4 justify-between cursor-pointer\"\n                  onClick={moveDown}\n                >\n                  Move Down <ArrowDown className=\"w-4 h-4\" />\n                </DropdownMenuItem>\n              )}\n\n              {(moveUp ?? moveDown) && <DropdownMenuSeparator />}\n\n              <DropdownMenuItem\n                className=\"flex gap-4 justify-between cursor-pointer\"\n                onClick={insertAbove}\n              >\n                Insert Above{\" \"}\n                <div className=\"flex\">\n                  <ArrowUp className=\"w-4 h-4\" />\n                  <Plus className=\"w-4 h-4\" />\n                </div>\n              </DropdownMenuItem>\n              <DropdownMenuItem\n                className=\"flex gap-4 justify-between cursor-pointer\"\n                onClick={insertBelow}\n              >\n                Insert Below{\" \"}\n                <div className=\"flex\">\n                  <ArrowDown className=\"w-4 h-4\" />\n                  <Plus className=\"w-4 h-4\" />\n                </div>\n              </DropdownMenuItem>\n\n              <DropdownMenuSeparator />\n\n              <DropdownMenuItem\n                className=\"flex gap-4 justify-between cursor-pointer\"\n                onClick={removeStage}\n              >\n                Remove <Minus className=\"w-4 h-4\" />\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </div>\n      </div>\n      <DataTable\n        tableKey=\"procedure-stage-executions\"\n        data={stage.executions!}\n        noResults={\n          <Button\n            onClick={() =>\n              setStage({\n                ...stage,\n                executions: [default_enabled_execution()],\n              })\n            }\n            variant=\"secondary\"\n            disabled={disabled}\n          >\n            Add Execution\n          </Button>\n        }\n        columns={[\n          {\n            header: \"Execution\",\n            size: 250,\n            cell: ({ row: { original, index } }) => (\n              <ExecutionTypeSelector\n                disabled={disabled}\n                type={original.execution.type}\n                onSelect={(type) =>\n                  setStage({\n                    ...stage,\n                    executions: stage.executions!.map((item, i) =>\n                      i === index\n                        ? ({\n                            ...item,\n                            execution: {\n                              type,\n                              params:\n                                TARGET_COMPONENTS[\n                                  type as Types.Execution[\"type\"]\n                                ].params,\n                            },\n                          } as Types.EnabledExecution)\n                        : item,\n                    ),\n                  })\n                }\n              />\n            ),\n          },\n          {\n            header: \"Target\",\n            size: 250,\n            cell: ({\n              row: {\n                original: {\n                  execution: { type, params },\n                },\n                index,\n              },\n            }) => {\n              const Component = TARGET_COMPONENTS[type].Component;\n              return (\n                <Component\n                  disabled={disabled}\n                  params={params as any}\n                  setParams={(params: any) =>\n                    setStage({\n                      ...stage,\n                      executions: stage.executions!.map((item, i) =>\n                        i === index\n                          ? {\n                              ...item,\n                              execution: { type, params },\n                            }\n                          : item,\n                      ) as Types.EnabledExecution[],\n                    })\n                  }\n                />\n              );\n            },\n          },\n          {\n            header: \"Add / Remove\",\n            size: 150,\n            cell: ({ row: { index } }) => (\n              <div className=\"flex items-center gap-2\">\n                <Button\n                  variant=\"secondary\"\n                  onClick={() =>\n                    setStage({\n                      ...stage,\n                      executions: [\n                        ...stage.executions!.slice(0, index + 1),\n                        default_enabled_execution(),\n                        ...stage.executions!.slice(index + 1),\n                      ],\n                    })\n                  }\n                  disabled={disabled}\n                >\n                  <PlusCircle className=\"w-4 h-4\" />\n                </Button>\n                <Button\n                  variant=\"secondary\"\n                  onClick={() =>\n                    setStage({\n                      ...stage,\n                      executions: stage.executions!.filter(\n                        (_, i) => i !== index,\n                      ),\n                    })\n                  }\n                  disabled={disabled}\n                >\n                  <MinusCircle className=\"w-4 h-4\" />\n                </Button>\n              </div>\n            ),\n          },\n          {\n            header: \"Enabled\",\n            size: 100,\n            cell: ({\n              row: {\n                original: { enabled },\n                index,\n              },\n            }) => {\n              return (\n                <Switch\n                  checked={enabled}\n                  onClick={() =>\n                    setStage({\n                      ...stage,\n                      executions: stage.executions!.map((item, i) =>\n                        i === index ? { ...item, enabled: !enabled } : item,\n                      ),\n                    })\n                  }\n                  disabled={disabled}\n                />\n              );\n            },\n          },\n        ]}\n      />\n    </Card>\n  );\n};\n\nconst ExecutionTypeSelector = ({\n  type,\n  onSelect,\n  disabled,\n}: {\n  type: Types.Execution[\"type\"];\n  onSelect: (type: Types.Execution[\"type\"]) => void;\n  disabled: boolean;\n}) => {\n  const execution_types = Object.keys(TARGET_COMPONENTS).filter(\n    (c) => ![\"None\"].includes(c),\n  );\n\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const filtered = filterBySplit(execution_types, search, (item) => item);\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button variant=\"secondary\" className=\"flex gap-2\" disabled={disabled}>\n          {fmt_upper_camelcase(type)}\n          <ChevronsUpDown className=\"w-3 h-3\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[200px] max-h-[200px] p-0\" sideOffset={12}>\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Search Executions\"\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center pt-2\">\n              Empty.\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n            <CommandGroup className=\"overflow-auto\">\n              {filtered.map((type) => (\n                <CommandItem\n                  key={type}\n                  onSelect={() => onSelect(type as Types.Execution[\"type\"])}\n                  className=\"flex items-center justify-between\"\n                >\n                  <div className=\"p-1\">{fmt_upper_camelcase(type)}</div>\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nconst TARGET_COMPONENTS: ExecutionConfigs = {\n  None: {\n    params: {},\n    Component: () => <></>,\n  },\n  // Procedure\n  RunProcedure: {\n    params: { procedure: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Procedure\"\n        selected={params.procedure}\n        onSelect={(procedure) => setParams({ procedure })}\n        disabled={disabled}\n      />\n    ),\n  },\n  BatchRunProcedure: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match procedures\"\n        value={\n          params.pattern ||\n          \"# Match procedures by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  // Action\n  RunAction: {\n    params: { action: \"\", args: {} },\n    Component: ({ params, setParams, disabled }) => (\n      <div className=\"flex gap-2 items-center\">\n        <ResourceSelector\n          type=\"Action\"\n          selected={params.action}\n          onSelect={(action) => setParams({ action, args: params.args })}\n          disabled={disabled}\n        />\n        <TextUpdateMenuMonaco\n          title=\"Action Arguments (JSON)\"\n          value={JSON.stringify(params.args ?? {}, undefined, 2)}\n          onUpdate={(args) =>\n            setParams({ action: params.action, args: JSON.parse(args) })\n          }\n          disabled={disabled}\n          language=\"json\"\n          triggerChild={\n            <Button variant=\"secondary\" size=\"icon\">\n              <Settings2 className=\"w-4\" />\n            </Button>\n          }\n        />\n      </div>\n    ),\n  },\n  BatchRunAction: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match actions\"\n        value={\n          params.pattern ||\n          \"# Match actions by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  // Build\n  RunBuild: {\n    params: { build: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Build\"\n        selected={params.build}\n        onSelect={(build) => setParams({ build })}\n        disabled={disabled}\n      />\n    ),\n  },\n  BatchRunBuild: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match builds\"\n        value={\n          params.pattern ||\n          \"# Match builds by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  CancelBuild: {\n    params: { build: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Build\"\n        selected={params.build}\n        onSelect={(build) => setParams({ build })}\n        disabled={disabled}\n      />\n    ),\n  },\n  // Deployment\n  Deploy: {\n    params: { deployment: \"\" },\n    Component: ({ params, setParams, disabled }) => {\n      return (\n        <ResourceSelector\n          type=\"Deployment\"\n          selected={params.deployment}\n          onSelect={(deployment) => setParams({ deployment })}\n          disabled={disabled}\n        />\n      );\n    },\n  },\n  BatchDeploy: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match deployments\"\n        value={\n          params.pattern ||\n          \"# Match deployments by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  PullDeployment: {\n    params: { deployment: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Deployment\"\n        selected={params.deployment}\n        onSelect={(deployment) => setParams({ deployment })}\n        disabled={disabled}\n      />\n    ),\n  },\n  StartDeployment: {\n    params: { deployment: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Deployment\"\n        selected={params.deployment}\n        onSelect={(deployment) => setParams({ deployment })}\n        disabled={disabled}\n      />\n    ),\n  },\n  RestartDeployment: {\n    params: { deployment: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Deployment\"\n        selected={params.deployment}\n        onSelect={(deployment) => setParams({ deployment })}\n        disabled={disabled}\n      />\n    ),\n  },\n  PauseDeployment: {\n    params: { deployment: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Deployment\"\n        selected={params.deployment}\n        onSelect={(deployment) => setParams({ deployment })}\n        disabled={disabled}\n      />\n    ),\n  },\n  UnpauseDeployment: {\n    params: { deployment: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Deployment\"\n        selected={params.deployment}\n        onSelect={(deployment) => setParams({ deployment })}\n        disabled={disabled}\n      />\n    ),\n  },\n  StopDeployment: {\n    params: { deployment: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Deployment\"\n        selected={params.deployment}\n        onSelect={(id) => setParams({ deployment: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  DestroyDeployment: {\n    params: { deployment: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Deployment\"\n        selected={params.deployment}\n        onSelect={(deployment) => setParams({ deployment })}\n        disabled={disabled}\n      />\n    ),\n  },\n  BatchDestroyDeployment: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match deployments\"\n        value={\n          params.pattern ||\n          \"# Match deployments by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  // Stack\n  DeployStack: {\n    params: { stack: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Stack\"\n        selected={params.stack}\n        onSelect={(id) => setParams({ stack: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  BatchDeployStack: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match stacks\"\n        value={\n          params.pattern ||\n          \"# Match stacks by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  DeployStackIfChanged: {\n    params: { stack: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Stack\"\n        selected={params.stack}\n        onSelect={(id) => setParams({ stack: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  BatchDeployStackIfChanged: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match stacks\"\n        value={\n          params.pattern ||\n          \"# Match stacks by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  PullStack: {\n    params: { stack: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Stack\"\n        selected={params.stack}\n        onSelect={(id) => setParams({ stack: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  BatchPullStack: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match stacks\"\n        value={\n          params.pattern ||\n          \"# Match stacks by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  StartStack: {\n    params: { stack: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Stack\"\n        selected={params.stack}\n        onSelect={(id) => setParams({ stack: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  RestartStack: {\n    params: { stack: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Stack\"\n        selected={params.stack}\n        onSelect={(id) => setParams({ stack: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  PauseStack: {\n    params: { stack: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Stack\"\n        selected={params.stack}\n        onSelect={(id) => setParams({ stack: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  UnpauseStack: {\n    params: { stack: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Stack\"\n        selected={params.stack}\n        onSelect={(id) => setParams({ stack: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  StopStack: {\n    params: { stack: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Stack\"\n        selected={params.stack}\n        onSelect={(id) => setParams({ stack: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  DestroyStack: {\n    params: { stack: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Stack\"\n        selected={params.stack}\n        onSelect={(id) => setParams({ stack: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  BatchDestroyStack: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match stacks\"\n        value={\n          params.pattern ||\n          \"# Match stacks by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  RunStackService: {\n    params: {\n      stack: \"\",\n      service: \"\",\n      command: undefined,\n      no_tty: undefined,\n      no_deps: undefined,\n      detach: undefined,\n      service_ports: undefined,\n      env: undefined,\n      workdir: undefined,\n      user: undefined,\n      entrypoint: undefined,\n      pull: undefined,\n    },\n    Component: ({ params, setParams, disabled }) => {\n      const [open, setOpen] = useState(false);\n      // local mirrors to allow cancel without committing\n      const [stack, setStack] = useState(params.stack ?? \"\");\n      const [service, setService] = useState(params.service ?? \"\");\n      const [commandText, setCommand] = useState(\n        params.command && params.command.length\n          ? shellQuote(params.command)\n          : \"\"\n      );\n      const [no_tty, setNoTty] = useState(!!params.no_tty);\n      const [no_deps, setNoDeps] = useState(!!params.no_deps);\n      const [detach, setDetach] = useState(!!params.detach);\n      const [service_ports, setServicePorts] = useState(!!params.service_ports);\n      const [workdir, setWorkdir] = useState(params.workdir ?? \"\");\n      const [user, setUser] = useState(params.user ?? \"\");\n      const [entrypoint, setEntrypoint] = useState(params.entrypoint ?? \"\");\n      const [pull, setPull] = useState(!!params.pull);\n      const env_text = (\n        params.env\n          ? Object.entries(params.env)\n              .map(([k, v]) => `${k}=${v}`)\n              .join(\"\\n\")\n          : \"  # VARIABLE = value\\n\"\n      ) as string;\n      const [envText, setEnvText] = useState(env_text);\n\n      useEffect(() => {\n        setStack(params.stack ?? \"\");\n        setService(params.service ?? \"\");\n        setCommand(\n          params.command && params.command.length\n            ? shellQuote(params.command)\n            : \"\"\n        );\n        setNoTty(!!params.no_tty);\n        setNoDeps(!!params.no_deps);\n        setDetach(!!params.detach);\n        setServicePorts(!!params.service_ports);\n        setWorkdir(params.workdir ?? \"\");\n        setUser(params.user ?? \"\");\n        setEntrypoint(params.entrypoint ?? \"\");\n        setPull(!!params.pull);\n        setEnvText(\n          params.env\n            ? Object.entries(params.env)\n                .map(([k, v]) => `${k}=${v}`)\n                .join(\"\\n\")\n            : \"  # VARIABLE = value\\n\"\n        );\n      }, [params]);\n\n      const onConfirm = () => {\n        const envArray = text_to_env(envText);\n        const env = envArray.length\n          ? envArray.reduce<Record<string, string>>(\n              (acc, { variable, value }) => {\n                if (variable) acc[variable] = value;\n                return acc;\n              },\n              {}\n            )\n          : undefined;\n        const parsed = commandText.trim()\n          ? shellParse(commandText.trim()).map((tok) =>\n              typeof tok === \"string\" ? tok : ((tok as any).op ?? String(tok))\n            )\n          : [];\n        setParams({\n          stack,\n          service,\n          command: parsed.length ? (parsed as string[]) : undefined,\n          no_tty: no_tty ? true : undefined,\n          no_deps: no_deps ? true : undefined,\n          service_ports: service_ports ? true : undefined,\n          workdir: workdir || undefined,\n          user: user || undefined,\n          entrypoint: entrypoint || undefined,\n          pull: pull ? true : undefined,\n          detach: detach ? true : undefined,\n          env,\n        } as any);\n        setOpen(false);\n      };\n\n      return (\n        <Dialog open={open} onOpenChange={setOpen}>\n          <DialogTrigger asChild>\n            <Button variant=\"secondary\" disabled={disabled}>\n              Configure\n            </Button>\n          </DialogTrigger>\n          <DialogContent className=\"w-[800px] max-w-[95vw] max-h-[80vh] overflow-auto\">\n            <DialogHeader>\n              <DialogTitle>Run Stack Service</DialogTitle>\n            </DialogHeader>\n            <div className=\"flex flex-col gap-3 py-2\">\n              <div className=\"grid grid-cols-1 md:grid-cols-2 gap-3\">\n                <div className=\"flex flex-col gap-1\">\n                  <div className=\"text-muted-foreground text-xs\">Stack</div>\n                  <ResourceSelector\n                    type=\"Stack\"\n                    selected={stack}\n                    onSelect={(id) => setStack(id)}\n                    disabled={disabled}\n                    align=\"start\"\n                  />\n                </div>\n                <div className=\"flex flex-col gap-1\">\n                  <div className=\"text-muted-foreground text-xs\">Service</div>\n                  <Input\n                    placeholder=\"service name\"\n                    value={service}\n                    onChange={(e) => setService(e.target.value)}\n                    disabled={disabled}\n                  />\n                </div>\n              </div>\n\n              <div className=\"flex flex-col gap-1\">\n                <div className=\"text-muted-foreground text-xs\">Command</div>\n                <Input\n                  value={commandText}\n                  onChange={(e) => setCommand(e.target.value)}\n                  disabled={disabled}\n                />\n              </div>\n\n              <div className=\"grid grid-cols-2 md:grid-cols-4 gap-3\">\n                <label className=\"flex items-center gap-2\">\n                  <Switch checked={no_tty} onCheckedChange={setNoTty} />\n                  <span className=\"text-sm\">No TTY</span>\n                </label>\n                <label className=\"flex items-center gap-2\">\n                  <Switch checked={no_deps} onCheckedChange={setNoDeps} />\n                  <span className=\"text-sm\">No Dependencies</span>\n                </label>\n                <label className=\"flex items-center gap-2\">\n                  <Switch checked={detach} onCheckedChange={setDetach} />\n                  <span className=\"text-sm\">Detach</span>\n                </label>\n                <label className=\"flex items-center gap-2\">\n                  <Switch\n                    checked={service_ports}\n                    onCheckedChange={setServicePorts}\n                  />\n                  <span className=\"text-sm\">Service Ports</span>\n                </label>\n                <label className=\"flex items-center gap-2\">\n                  <Switch checked={pull} onCheckedChange={setPull} />\n                  <span className=\"text-sm\">Pull Image</span>\n                </label>\n              </div>\n\n              <div className=\"grid grid-cols-1 md:grid-cols-2 gap-3\">\n                <div className=\"flex flex-col gap-1\">\n                  <div className=\"text-muted-foreground text-xs\">\n                    Working Directory\n                  </div>\n                  <Input\n                    placeholder=\"/work/dir\"\n                    value={workdir}\n                    onChange={(e) => setWorkdir(e.target.value)}\n                    disabled={disabled}\n                  />\n                </div>\n                <div className=\"flex flex-col gap-1\">\n                  <div className=\"text-muted-foreground text-xs\">User</div>\n                  <Input\n                    placeholder=\"uid:gid or user\"\n                    value={user}\n                    onChange={(e) => setUser(e.target.value)}\n                    disabled={disabled}\n                  />\n                </div>\n                <div className=\"flex flex-col gap-1 md:col-span-2\">\n                  <div className=\"text-muted-foreground text-xs\">\n                    Entrypoint\n                  </div>\n                  <Input\n                    value={entrypoint}\n                    onChange={(e) => setEntrypoint(e.target.value)}\n                    disabled={disabled}\n                  />\n                </div>\n              </div>\n\n              <div className=\"flex flex-col gap-1\">\n                <div className=\"text-muted-foreground text-xs\">\n                  Extra Environment Variables\n                </div>\n                <MonacoEditor\n                  value={envText}\n                  onValueChange={setEnvText}\n                  language=\"key_value\"\n                  readOnly={disabled}\n                />\n              </div>\n            </div>\n            <DialogFooter>\n              <Button\n                variant=\"secondary\"\n                disabled={disabled}\n                onClick={onConfirm}\n                className=\"flex items-center gap-2\"\n              >\n                <CheckCircle className=\"w-4 h-4\" /> Confirm\n              </Button>\n            </DialogFooter>\n          </DialogContent>\n        </Dialog>\n      );\n    },\n  },\n  // Repo\n  CloneRepo: {\n    params: { repo: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Repo\"\n        selected={params.repo}\n        onSelect={(repo) => setParams({ repo })}\n        disabled={disabled}\n      />\n    ),\n  },\n  BatchCloneRepo: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match repos\"\n        value={\n          params.pattern ||\n          \"# Match repos by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  PullRepo: {\n    params: { repo: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Repo\"\n        selected={params.repo}\n        onSelect={(repo) => setParams({ repo })}\n        disabled={disabled}\n      />\n    ),\n  },\n  BatchPullRepo: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match repos\"\n        value={\n          params.pattern ||\n          \"# Match repos by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  BuildRepo: {\n    params: { repo: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Repo\"\n        selected={params.repo}\n        onSelect={(repo) => setParams({ repo })}\n        disabled={disabled}\n      />\n    ),\n  },\n  BatchBuildRepo: {\n    params: { pattern: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Match repos\"\n        value={\n          params.pattern ||\n          \"# Match repos by name, id, wildcard, or \\\\regex\\\\.\\n\"\n        }\n        onUpdate={(pattern) => setParams({ pattern })}\n        disabled={disabled}\n        language=\"string_list\"\n        fullWidth\n      />\n    ),\n  },\n  CancelRepoBuild: {\n    params: { repo: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Repo\"\n        selected={params.repo}\n        onSelect={(repo) => setParams({ repo })}\n        disabled={disabled}\n      />\n    ),\n  },\n  // Server\n  // StartContainer: {\n  //   params: { server: \"\" },\n  //   Component: ({ params, setParams, disabled }) => (\n  //     <ResourceSelector\n  //       type=\"Server\"\n  //       selected={params.server}\n  //       onSelect={(server) => setParams({ server })}\n  //       disabled={disabled}\n  //     />\n  //   ),\n  // },\n  // RestartContainer: {\n  //   params: { server: \"\" },\n  //   Component: ({ params, setParams, disabled }) => (\n  //     <ResourceSelector\n  //       type=\"Server\"\n  //       selected={params.server}\n  //       onSelect={(server) => setParams({ server })}\n  //       disabled={disabled}\n  //     />\n  //   ),\n  // },\n  // PauseContainer: {\n  //   params: { server: \"\" },\n  //   Component: ({ params, setParams, disabled }) => (\n  //     <ResourceSelector\n  //       type=\"Server\"\n  //       selected={params.server}\n  //       onSelect={(server) => setParams({ server })}\n  //       disabled={disabled}\n  //     />\n  //   ),\n  // },\n  // UnpauseContainer: {\n  //   params: { server: \"\" },\n  //   Component: ({ params, setParams, disabled }) => (\n  //     <ResourceSelector\n  //       type=\"Server\"\n  //       selected={params.server}\n  //       onSelect={(server) => setParams({ server })}\n  //       disabled={disabled}\n  //     />\n  //   ),\n  // },\n  // StopContainer: {\n  //   params: { server: \"\" },\n  //   Component: ({ params, setParams, disabled }) => (\n  //     <ResourceSelector\n  //       type=\"Server\"\n  //       selected={params.server}\n  //       onSelect={(server) => setParams({ server })}\n  //       disabled={disabled}\n  //     />\n  //   ),\n  // },\n  // DestroyContainer: {\n  //   params: { server: \"\", container: \"\" },\n  //   Component: ({ params, setParams, disabled }) => (\n  //     <ResourceSelector\n  //       type=\"Server\"\n  //       selected={params.server}\n  //       onSelect={(server) => setParams({ server })}\n  //       disabled={disabled}\n  //     />\n  //   ),\n  // },\n  StartAllContainers: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(id) => setParams({ server: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  RestartAllContainers: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(id) => setParams({ server: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  PauseAllContainers: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(id) => setParams({ server: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  UnpauseAllContainers: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(id) => setParams({ server: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  StopAllContainers: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(id) => setParams({ server: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  PruneContainers: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(server) => setParams({ server })}\n        disabled={disabled}\n      />\n    ),\n  },\n  PruneNetworks: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(server) => setParams({ server })}\n        disabled={disabled}\n      />\n    ),\n  },\n  PruneImages: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(server) => setParams({ server })}\n        disabled={disabled}\n      />\n    ),\n  },\n  PruneVolumes: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(server) => setParams({ server })}\n        disabled={disabled}\n      />\n    ),\n  },\n  PruneDockerBuilders: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(server) => setParams({ server })}\n        disabled={disabled}\n      />\n    ),\n  },\n  PruneBuildx: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(server) => setParams({ server })}\n        disabled={disabled}\n      />\n    ),\n  },\n  PruneSystem: {\n    params: { server: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"Server\"\n        selected={params.server}\n        onSelect={(server) => setParams({ server })}\n        disabled={disabled}\n      />\n    ),\n  },\n  RunSync: {\n    params: { sync: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"ResourceSync\"\n        selected={params.sync}\n        onSelect={(id) => setParams({ sync: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n  CommitSync: {\n    params: { sync: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <ResourceSelector\n        type=\"ResourceSync\"\n        selected={params.sync}\n        onSelect={(id) => setParams({ sync: id })}\n        disabled={disabled}\n      />\n    ),\n  },\n\n  ClearRepoCache: {\n    params: {},\n    Component: () => <></>,\n  },\n  BackupCoreDatabase: {\n    params: {},\n    Component: () => <></>,\n  },\n  GlobalAutoUpdate: {\n    params: {},\n    Component: () => <></>,\n  },\n\n  SendAlert: {\n    params: { message: \"\" },\n    Component: ({ params, setParams, disabled }) => (\n      <TextUpdateMenuMonaco\n        title=\"Alert message\"\n        value={params.message}\n        placeholder=\"Configure custom alert message\"\n        onUpdate={(message) => setParams({ message })}\n        disabled={disabled}\n        language={undefined}\n        fullWidth\n      />\n    ),\n  },\n\n  Sleep: {\n    params: { duration_ms: 0 },\n    Component: ({ params, setParams, disabled }) => {\n      const { toast } = useToast();\n      const [internal, setInternal] = useState(\n        params.duration_ms?.toString() ?? \"\"\n      );\n      useEffect(() => {\n        setInternal(params.duration_ms?.toString() ?? \"\");\n      }, [params.duration_ms]);\n      return (\n        <Input\n          placeholder=\"Duration in milliseconds\"\n          value={internal}\n          onChange={(e) => setInternal(e.target.value)}\n          onBlur={() => {\n            const duration_ms = Number(internal);\n            if (duration_ms) {\n              setParams({ duration_ms });\n            } else {\n              toast({\n                title: \"Duration must be valid number\",\n                variant: \"destructive\",\n              });\n            }\n          }}\n          disabled={disabled}\n        />\n      );\n    },\n  },\n};\n"
  },
  {
    "path": "frontend/src/components/resources/procedure/index.tsx",
    "content": "import { ActionWithDialog, StatusBadge } from \"@components/util\";\nimport { useExecute, useRead } from \"@lib/hooks\";\nimport { RequiredResourceComponents } from \"@types\";\nimport { Clock, Route } from \"lucide-react\";\nimport { ProcedureConfig } from \"./config\";\nimport { ProcedureTable } from \"./table\";\nimport { DeleteResource, NewResource, ResourcePageHeader } from \"../common\";\nimport {\n  procedure_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\nimport { cn, updateLogToHtml } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { DashboardPieChart } from \"@pages/home/dashboard\";\nimport { GroupActions } from \"@components/group-actions\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@ui/tooltip\";\nimport { Card } from \"@ui/card\";\n\nconst useProcedure = (id?: string) =>\n  useRead(\"ListProcedures\", {}).data?.find((d) => d.id === id);\n\nconst ProcedureIcon = ({ id, size }: { id?: string; size: number }) => {\n  const state = useProcedure(id)?.info.state;\n  const color = stroke_color_class_by_intention(\n    procedure_state_intention(state)\n  );\n  return <Route className={cn(`w-${size} h-${size}`, state && color)} />;\n};\n\nexport const ProcedureComponents: RequiredResourceComponents = {\n  list_item: (id) => useProcedure(id),\n  resource_links: () => undefined,\n\n  Description: () => <>Orchestrate multiple Komodo executions.</>,\n\n  Dashboard: () => {\n    const summary = useRead(\"GetProceduresSummary\", {}).data;\n    return (\n      <DashboardPieChart\n        data={[\n          { title: \"Ok\", intention: \"Good\", value: summary?.ok ?? 0 },\n          {\n            title: \"Running\",\n            intention: \"Warning\",\n            value: summary?.running ?? 0,\n          },\n          {\n            title: \"Failed\",\n            intention: \"Critical\",\n            value: summary?.failed ?? 0,\n          },\n          {\n            title: \"Unknown\",\n            intention: \"Unknown\",\n            value: summary?.unknown ?? 0,\n          },\n        ]}\n      />\n    );\n  },\n\n  New: () => <NewResource type=\"Procedure\" />,\n\n  GroupActions: () => (\n    <GroupActions type=\"Procedure\" actions={[\"RunProcedure\"]} />\n  ),\n\n  Table: ({ resources }) => (\n    <ProcedureTable procedures={resources as Types.ProcedureListItem[]} />\n  ),\n\n  Icon: ({ id }) => <ProcedureIcon id={id} size={4} />,\n  BigIcon: ({ id }) => <ProcedureIcon id={id} size={8} />,\n\n  State: ({ id }) => {\n    let state = useProcedure(id)?.info.state;\n    return (\n      <StatusBadge text={state} intent={procedure_state_intention(state)} />\n    );\n  },\n\n  Status: {},\n\n  Info: {\n    Schedule: ({ id }) => {\n      const next_scheduled_run = useProcedure(id)?.info.next_scheduled_run;\n      return (\n        <div className=\"flex gap-2 items-center\">\n          <Clock className=\"w-4 h-4\" />\n          Next Run:\n          <div className=\"font-bold\">\n            {next_scheduled_run\n              ? new Date(next_scheduled_run).toLocaleString()\n              : \"Not Scheduled\"}\n          </div>\n        </div>\n      );\n    },\n    ScheduleErrors: ({ id }) => {\n      const error = useProcedure(id)?.info.schedule_error;\n      if (!error) {\n        return null;\n      }\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card className=\"px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer\">\n              <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                Schedule Error\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent className=\"w-[400px]\">\n            <pre\n              dangerouslySetInnerHTML={{\n                __html: updateLogToHtml(error),\n              }}\n              className=\"max-h-[500px] overflow-y-auto\"\n            />\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n  },\n\n  Actions: {\n    RunProcedure: ({ id }) => {\n      const running = useRead(\n        \"GetProcedureActionState\",\n        { procedure: id },\n        { refetchInterval: 5000 }\n      ).data?.running;\n      const { mutate, isPending } = useExecute(\"RunProcedure\");\n      const procedure = useProcedure(id);\n      if (!procedure) return null;\n      return (\n        <ActionWithDialog\n          name={procedure.name}\n          title={running ? \"Running\" : \"Run Procedure\"}\n          icon={<Route className=\"h-4 w-4\" />}\n          onClick={() => mutate({ procedure: id })}\n          disabled={running || isPending}\n          loading={running}\n        />\n      );\n    },\n  },\n\n  Page: {},\n\n  Config: ProcedureConfig,\n\n  DangerZone: ({ id }) => <DeleteResource type=\"Procedure\" id={id} />,\n\n  ResourcePageHeader: ({ id }) => {\n    const procedure = useProcedure(id);\n\n    return (\n      <ResourcePageHeader\n        intent={procedure_state_intention(procedure?.info.state)}\n        icon={<ProcedureIcon id={id} size={8} />}\n        type=\"Procedure\"\n        id={id}\n        resource={procedure}\n        state={procedure?.info.state}\n        status={`${procedure?.info.stages} Stage${procedure?.info.stages === 1 ? \"\" : \"s\"}`}\n      />\n    );\n  },\n};\n"
  },
  {
    "path": "frontend/src/components/resources/procedure/table.tsx",
    "content": "import { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { TableTags } from \"@components/tags\";\nimport { ResourceLink } from \"../common\";\nimport { ProcedureComponents } from \".\";\nimport { Types } from \"komodo_client\";\nimport { useSelectedResources } from \"@lib/hooks\";\n\nexport const ProcedureTable = ({\n  procedures,\n}: {\n  procedures: Types.ProcedureListItem[];\n}) => {\n  const [_, setSelectedResources] = useSelectedResources(\"Procedure\");\n\n  return (\n    <DataTable\n      tableKey=\"procedures\"\n      data={procedures}\n      selectOptions={{\n        selectKey: ({ name }) => name,\n        onSelect: setSelectedResources,\n      }}\n      columns={[\n        {\n          accessorKey: \"name\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Name\" />\n          ),\n          cell: ({ row }) => (\n            <ResourceLink type=\"Procedure\" id={row.original.id} />\n          ),\n        },\n        {\n          accessorKey: \"info.state\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"State\" />\n          ),\n          cell: ({ row }) => <ProcedureComponents.State id={row.original.id} />,\n        },\n        {\n          accessorKey: \"info.next_scheduled_run\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Next Run\" />\n          ),\n          sortingFn: (a, b) => {\n            const sa = a.original.info.next_scheduled_run;\n            const sb = b.original.info.next_scheduled_run;\n\n            if (!sa && !sb) return 0;\n            if (!sa) return 1;\n            if (!sb) return -1;\n\n            if (sa > sb) return 1;\n            else if (sa < sb) return -1;\n            else return 0;\n          },\n          cell: ({ row }) =>\n            row.original.info.next_scheduled_run\n              ? new Date(row.original.info.next_scheduled_run).toLocaleString()\n              : \"Not Scheduled\",\n        },\n        {\n          header: \"Tags\",\n          cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/repo/actions.tsx",
    "content": "import { ConfirmButton } from \"@components/util\";\nimport { useExecute, usePermissions, useRead } from \"@lib/hooks\";\nimport {\n  ArrowDownToDot,\n  ArrowDownToLine,\n  Ban,\n  Hammer,\n  Loader2,\n} from \"lucide-react\";\nimport { useRepo } from \".\";\nimport { Types } from \"komodo_client\";\nimport { useBuilder } from \"../builder\";\n\nexport const CloneRepo = ({ id }: { id: string }) => {\n  const { mutate, isPending } = useExecute(\"CloneRepo\");\n  const cloning = useRead(\n    \"GetRepoActionState\",\n    { repo: id },\n    { refetchInterval: 5000 }\n  ).data?.cloning;\n  const info = useRepo(id)?.info;\n  if (!info?.server_id) return null;\n  const hash = info?.latest_hash;\n  const isCloned = (hash?.length || 0) > 0;\n  const pending = isPending || cloning;\n  return (\n    <ConfirmButton\n      title={isCloned ? \"Reclone\" : \"Clone\"}\n      icon={\n        pending ? (\n          <Loader2 className=\"w-4 h-4 animate-spin\" />\n        ) : (\n          <ArrowDownToLine className=\"w-4 h-4\" />\n        )\n      }\n      onClick={() => mutate({ repo: id })}\n      disabled={pending}\n      loading={pending}\n    />\n  );\n};\n\nexport const PullRepo = ({ id }: { id: string }) => {\n  const { mutate, isPending } = useExecute(\"PullRepo\");\n  const pulling = useRead(\n    \"GetRepoActionState\",\n    { repo: id },\n    { refetchInterval: 5000 }\n  ).data?.pulling;\n  const info = useRepo(id)?.info;\n  if (!info?.server_id) return null;\n  const hash = info?.latest_hash;\n  const isCloned = (hash?.length || 0) > 0;\n  if (!isCloned) return null;\n  const pending = isPending || pulling;\n  return (\n    <ConfirmButton\n      title=\"Pull\"\n      icon={\n        pending ? (\n          <Loader2 className=\"w-4 h-4 animate-spin\" />\n        ) : (\n          <ArrowDownToDot className=\"w-4 h-4\" />\n        )\n      }\n      onClick={() => mutate({ repo: id })}\n      disabled={pending}\n      loading={pending}\n    />\n  );\n};\n\nexport const BuildRepo = ({ id }: { id: string }) => {\n  const { canExecute } = usePermissions({ type: \"Repo\", id });\n  const building = useRead(\n    \"GetRepoActionState\",\n    { repo: id },\n    { refetchInterval: 5000 }\n  ).data?.building;\n  const updates = useRead(\"ListUpdates\", {\n    query: {\n      \"target.type\": \"Repo\",\n      \"target.id\": id,\n    },\n  }).data;\n  const { mutate: run_mutate, isPending: runPending } = useExecute(\"BuildRepo\");\n  const { mutate: cancel_mutate, isPending: cancelPending } =\n    useExecute(\"CancelRepoBuild\");\n\n  const repo = useRepo(id);\n  const builder = useBuilder(repo?.info.builder_id);\n  const canCancel = builder?.info.builder_type !== \"Server\";\n\n  // Don't show if builder not attached\n  if (!builder) return null;\n\n  // make sure hidden without perms.\n  // not usually necessary, but this button also used in deployment actions.\n  if (!canExecute) return null;\n\n  // updates come in in descending order, so 'find' will find latest update matching operation\n  const latestBuild = updates?.updates.find(\n    (u) => u.operation === Types.Operation.BuildRepo\n  );\n  const latestCancel = updates?.updates.find(\n    (u) => u.operation === Types.Operation.CancelRepoBuild\n  );\n  const cancelDisabled =\n    !canCancel ||\n    cancelPending ||\n    (latestCancel && latestBuild\n      ? latestCancel!.start_ts > latestBuild!.start_ts\n      : false);\n\n  if (building) {\n    return (\n      <ConfirmButton\n        title=\"Cancel Build\"\n        variant=\"destructive\"\n        icon={<Ban className=\"h-4 w-4\" />}\n        onClick={() => cancel_mutate({ repo: id })}\n        disabled={cancelDisabled}\n      />\n    );\n  } else {\n    return (\n      <ConfirmButton\n        title=\"Build\"\n        icon={\n          runPending ? (\n            <Loader2 className=\"w-4 h-4 animate-spin\" />\n          ) : (\n            <Hammer className=\"h-4 w-4\" />\n          )\n        }\n        onClick={() => run_mutate({ repo: id })}\n        disabled={runPending}\n      />\n    );\n  }\n};\n"
  },
  {
    "path": "frontend/src/components/resources/repo/config.tsx",
    "content": "import { Config } from \"@components/config\";\nimport {\n  AccountSelectorConfig,\n  ConfigItem,\n  InputList,\n  ProviderSelectorConfig,\n  SystemCommand,\n  WebhookBuilder,\n} from \"@components/config/util\";\nimport {\n  getWebhookIntegration,\n  useInvalidate,\n  useLocalStorage,\n  usePermissions,\n  useRead,\n  useWebhookIdOrName,\n  useWebhookIntegrations,\n  useWrite,\n} from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { CopyWebhook, ResourceLink, ResourceSelector } from \"../common\";\nimport { useToast } from \"@ui/use-toast\";\nimport { text_color_class_by_intention } from \"@lib/color\";\nimport { ConfirmButton } from \"@components/util\";\nimport { Ban, CirclePlus, PlusCircle } from \"lucide-react\";\nimport { Button } from \"@ui/button\";\nimport { SecretsSearch } from \"@components/config/env_vars\";\nimport { MonacoEditor } from \"@components/monaco\";\n\nexport const RepoConfig = ({ id }: { id: string }) => {\n  const { canWrite } = usePermissions({ type: \"Repo\", id });\n  const repo = useRead(\"GetRepo\", { repo: id }).data;\n  const config = repo?.config;\n  const name = repo?.name;\n  const webhooks = useRead(\"GetRepoWebhooksEnabled\", { repo: id }).data;\n  const global_disabled =\n    useRead(\"GetCoreInfo\", {}).data?.ui_write_disabled ?? false;\n  const [update, set] = useLocalStorage<Partial<Types.RepoConfig>>(\n    `repo-${id}-update-v1`,\n    {}\n  );\n  const { mutateAsync } = useWrite(\"UpdateRepo\");\n  const { integrations } = useWebhookIntegrations();\n  const [id_or_name] = useWebhookIdOrName();\n\n  if (!config) return null;\n\n  const disabled = global_disabled || !canWrite;\n\n  const git_provider = update.git_provider ?? config.git_provider;\n  const webhook_integration = getWebhookIntegration(integrations, git_provider);\n\n  return (\n    <Config\n      disabled={disabled}\n      original={config}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: update });\n      }}\n      components={{\n        \"\": [\n          {\n            label: \"Server\",\n            labelHidden: true,\n            components: {\n              server_id: (server_id, set) => {\n                return (\n                  <ConfigItem\n                    label={\n                      server_id ? (\n                        <div className=\"flex gap-3 text-lg\">\n                          Server:\n                          <ResourceLink type=\"Server\" id={server_id} />\n                        </div>\n                      ) : (\n                        \"Select Server\"\n                      )\n                    }\n                    description=\"Select the Server to clone on.\"\n                  >\n                    <ResourceSelector\n                      type=\"Server\"\n                      selected={server_id}\n                      onSelect={(server_id) => set({ server_id })}\n                      disabled={disabled}\n                      align=\"start\"\n                    />\n                  </ConfigItem>\n                );\n              },\n            },\n          },\n          {\n            label: \"Builder\",\n            labelHidden: true,\n            components: {\n              builder_id: (builder_id, set) => {\n                return (\n                  <ConfigItem\n                    label={\n                      builder_id ? (\n                        <div className=\"flex gap-3 text-lg\">\n                          Builder:\n                          <ResourceLink type=\"Builder\" id={builder_id} />\n                        </div>\n                      ) : (\n                        \"Select Builder\"\n                      )\n                    }\n                    description=\"Select the Builder to build with.\"\n                  >\n                    <ResourceSelector\n                      type=\"Builder\"\n                      selected={builder_id}\n                      onSelect={(builder_id) => set({ builder_id })}\n                      disabled={disabled}\n                      align=\"start\"\n                    />\n                  </ConfigItem>\n                );\n              },\n            },\n          },\n          {\n            label: \"Source\",\n            components: {\n              git_provider: (provider, set) => {\n                const https = update.git_https ?? config.git_https;\n                return (\n                  <ProviderSelectorConfig\n                    account_type=\"git\"\n                    selected={provider}\n                    disabled={disabled}\n                    onSelect={(git_provider) => set({ git_provider })}\n                    https={https}\n                    onHttpsSwitch={() => set({ git_https: !https })}\n                  />\n                );\n              },\n              git_account: (account, set) => (\n                <AccountSelectorConfig\n                  id={update.builder_id ?? config.builder_id ?? undefined}\n                  type=\"Builder\"\n                  account_type=\"git\"\n                  provider={update.git_provider ?? config.git_provider}\n                  selected={account}\n                  onSelect={(git_account) => set({ git_account })}\n                  disabled={disabled}\n                  placeholder=\"None\"\n                />\n              ),\n              repo: {\n                placeholder: \"Enter repo\",\n                description:\n                  \"The repo path on the provider. {namespace}/{repo_name}\",\n              },\n              branch: {\n                placeholder: \"Enter branch\",\n                description: \"Select a custom branch, or default to 'main'.\",\n              },\n              commit: {\n                label: \"Commit Hash\",\n                placeholder: \"Input commit hash\",\n                description:\n                  \"Optional. Switch to a specific commit hash after cloning the branch.\",\n              },\n            },\n          },\n          {\n            label: \"Path\",\n            labelHidden: true,\n            components: {\n              path: {\n                label: \"Clone Path\",\n                boldLabel: true,\n                placeholder: \"/clone/path/on/host\",\n                description: (\n                  <div className=\"flex flex-col gap-0\">\n                    <div>\n                      Explicitly specify the folder on the host to clone the\n                      repo in.\n                    </div>\n                    <div>\n                      If <span className=\"font-bold\">relative</span> (no leading\n                      '/'), relative to {\"$root_directory/repos/\" + repo.name}\n                    </div>\n                  </div>\n                ),\n              },\n            },\n          },\n          {\n            label: \"Environment\",\n            description:\n              \"Write these variables to a .env-formatted file at the specified path, before on_clone / on_pull are run.\",\n            components: {\n              environment: (env, set) => (\n                <div className=\"flex flex-col gap-4\">\n                  <SecretsSearch\n                    server={update.server_id ?? config.server_id}\n                  />\n                  <MonacoEditor\n                    value={env || \"  # VARIABLE = value\\n\"}\n                    onValueChange={(environment) => set({ environment })}\n                    language=\"key_value\"\n                    readOnly={disabled}\n                  />\n                </div>\n              ),\n              env_file_path: {\n                description:\n                  \"The path to write the file to, relative to the root of the repo.\",\n                placeholder: \".env\",\n              },\n              // skip_secret_interp: true,\n            },\n          },\n          {\n            label: \"On Clone\",\n            description:\n              \"Execute a shell command after cloning the repo. The given Cwd is relative to repo root.\",\n            components: {\n              on_clone: (value, set) => (\n                <SystemCommand\n                  value={value}\n                  set={(value) => set({ on_clone: value })}\n                  disabled={disabled}\n                />\n              ),\n            },\n          },\n          {\n            label: \"On Pull\",\n            description:\n              \"Execute a shell command after pulling the repo. The given Cwd is relative to repo root.\",\n            components: {\n              on_pull: (value, set) => (\n                <SystemCommand\n                  value={value}\n                  set={(value) => set({ on_pull: value })}\n                  disabled={disabled}\n                />\n              ),\n            },\n          },\n          {\n            label: \"Webhooks\",\n            description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,\n            components: {\n              [\"Guard\" as any]: () => {\n                if (update.branch ?? config.branch) {\n                  return null;\n                }\n                return (\n                  <ConfigItem label=\"Configure Branch\">\n                    <div>Must configure Branch before webhooks will work.</div>\n                  </ConfigItem>\n                );\n              },\n              [\"Builder\" as any]: () => (\n                <WebhookBuilder git_provider={git_provider} />\n              ),\n              [\"pull\" as any]: () => (\n                <ConfigItem label=\"Webhook Url - Pull\">\n                  <CopyWebhook\n                    integration={webhook_integration}\n                    path={`/repo/${id_or_name === \"Id\" ? id : encodeURIComponent(name ?? \"...\")}/pull`}\n                  />\n                </ConfigItem>\n              ),\n              [\"clone\" as any]: () => (\n                <ConfigItem label=\"Webhook Url - Clone\">\n                  <CopyWebhook\n                    integration={webhook_integration}\n                    path={`/repo/${id_or_name === \"Id\" ? id : encodeURIComponent(name ?? \"...\")}/clone`}\n                  />\n                </ConfigItem>\n              ),\n              [\"build\" as any]: () => (\n                <ConfigItem label=\"Webhook Url - Build\">\n                  <CopyWebhook\n                    integration={webhook_integration}\n                    path={`/repo/${id_or_name === \"Id\" ? id : encodeURIComponent(name ?? \"...\")}/build`}\n                  />\n                </ConfigItem>\n              ),\n              webhook_enabled: webhooks !== undefined && !webhooks.managed,\n              webhook_secret: {\n                description:\n                  \"Provide a custom webhook secret for this resource, or use the global default.\",\n                placeholder: \"Input custom secret\",\n              },\n              [\"managed\" as any]: () => {\n                const inv = useInvalidate();\n                const { toast } = useToast();\n                const { mutate: createWebhook, isPending: createPending } =\n                  useWrite(\"CreateRepoWebhook\", {\n                    onSuccess: () => {\n                      toast({ title: \"Webhook Created\" });\n                      inv([\"GetRepoWebhooksEnabled\", { repo: id }]);\n                    },\n                  });\n                const { mutate: deleteWebhook, isPending: deletePending } =\n                  useWrite(\"DeleteRepoWebhook\", {\n                    onSuccess: () => {\n                      toast({ title: \"Webhook Deleted\" });\n                      inv([\"GetRepoWebhooksEnabled\", { repo: id }]);\n                    },\n                  });\n                if (!webhooks || !webhooks.managed) return;\n                return (\n                  <ConfigItem label=\"Manage Webhook\">\n                    {webhooks.clone_enabled && (\n                      <div className=\"flex items-center gap-4 flex-wrap\">\n                        <div className=\"flex items-center gap-2\">\n                          Incoming webhook is{\" \"}\n                          <div\n                            className={text_color_class_by_intention(\"Good\")}\n                          >\n                            ENABLED\n                          </div>\n                          and will trigger\n                          <div\n                            className={text_color_class_by_intention(\"Neutral\")}\n                          >\n                            CLONE\n                          </div>\n                        </div>\n                        <ConfirmButton\n                          title=\"Disable\"\n                          icon={<Ban className=\"w-4 h-4\" />}\n                          variant=\"destructive\"\n                          onClick={() =>\n                            deleteWebhook({\n                              repo: id,\n                              action: Types.RepoWebhookAction.Clone,\n                            })\n                          }\n                          loading={deletePending}\n                          disabled={disabled || deletePending}\n                        />\n                      </div>\n                    )}\n                    {!webhooks.clone_enabled && webhooks.pull_enabled && (\n                      <div className=\"flex items-center gap-4 flex-wrap\">\n                        <div className=\"flex items-center gap-2\">\n                          Incoming webhook is{\" \"}\n                          <div\n                            className={text_color_class_by_intention(\"Good\")}\n                          >\n                            ENABLED\n                          </div>\n                          and will trigger\n                          <div\n                            className={text_color_class_by_intention(\"Neutral\")}\n                          >\n                            PULL\n                          </div>\n                        </div>\n                        <ConfirmButton\n                          title=\"Disable\"\n                          icon={<Ban className=\"w-4 h-4\" />}\n                          variant=\"destructive\"\n                          onClick={() =>\n                            deleteWebhook({\n                              repo: id,\n                              action: Types.RepoWebhookAction.Pull,\n                            })\n                          }\n                          loading={deletePending}\n                          disabled={disabled || deletePending}\n                        />\n                      </div>\n                    )}\n                    {webhooks.build_enabled && (\n                      <div className=\"flex items-center gap-4 flex-wrap\">\n                        <div className=\"flex items-center gap-2\">\n                          Incoming webhook is{\" \"}\n                          <div\n                            className={text_color_class_by_intention(\"Good\")}\n                          >\n                            ENABLED\n                          </div>\n                          and will trigger\n                          <div\n                            className={text_color_class_by_intention(\"Neutral\")}\n                          >\n                            BUILD\n                          </div>\n                        </div>\n                        <ConfirmButton\n                          title=\"Disable\"\n                          icon={<Ban className=\"w-4 h-4\" />}\n                          variant=\"destructive\"\n                          onClick={() =>\n                            deleteWebhook({\n                              repo: id,\n                              action: Types.RepoWebhookAction.Build,\n                            })\n                          }\n                          loading={deletePending}\n                          disabled={disabled || deletePending}\n                        />\n                      </div>\n                    )}\n                    {!webhooks.clone_enabled &&\n                      !webhooks.pull_enabled &&\n                      !webhooks.build_enabled && (\n                        <div className=\"flex items-center gap-4 flex-wrap\">\n                          <div className=\"flex items-center gap-2\">\n                            Incoming webhook is{\" \"}\n                            <div\n                              className={text_color_class_by_intention(\n                                \"Critical\"\n                              )}\n                            >\n                              DISABLED\n                            </div>\n                          </div>\n                          {(update.server_id ?? config.server_id) && (\n                            <ConfirmButton\n                              title=\"Enable Clone\"\n                              icon={<CirclePlus className=\"w-4 h-4\" />}\n                              onClick={() =>\n                                createWebhook({\n                                  repo: id,\n                                  action: Types.RepoWebhookAction.Clone,\n                                })\n                              }\n                              loading={createPending}\n                              disabled={disabled || createPending}\n                            />\n                          )}\n                          {(update.server_id ?? config.server_id) && (\n                            <ConfirmButton\n                              title=\"Enable Pull\"\n                              icon={<CirclePlus className=\"w-4 h-4\" />}\n                              onClick={() =>\n                                createWebhook({\n                                  repo: id,\n                                  action: Types.RepoWebhookAction.Pull,\n                                })\n                              }\n                              loading={createPending}\n                              disabled={disabled || createPending}\n                            />\n                          )}\n                          {(update.builder_id ?? config.builder_id) && (\n                            <ConfirmButton\n                              title=\"Enable Build\"\n                              icon={<CirclePlus className=\"w-4 h-4\" />}\n                              onClick={() =>\n                                createWebhook({\n                                  repo: id,\n                                  action: Types.RepoWebhookAction.Build,\n                                })\n                              }\n                              loading={createPending}\n                              disabled={disabled || createPending}\n                            />\n                          )}\n                        </div>\n                      )}\n                  </ConfigItem>\n                );\n              },\n            },\n          },\n          {\n            label: \"Links\",\n            description: \"Add quick links in the resource header\",\n            contentHidden: ((update.links ?? config.links)?.length ?? 0) === 0,\n            actions: !disabled && (\n              <Button\n                variant=\"secondary\"\n                onClick={() =>\n                  set((update) => ({\n                    ...update,\n                    links: [...(update.links ?? config.links ?? []), \"\"],\n                  }))\n                }\n                className=\"flex items-center gap-2 w-[200px]\"\n              >\n                <PlusCircle className=\"w-4 h-4\" />\n                Add Link\n              </Button>\n            ),\n            components: {\n              links: (values, set) => (\n                <InputList\n                  field=\"links\"\n                  values={values ?? []}\n                  set={set}\n                  disabled={disabled}\n                  placeholder=\"Input link\"\n                />\n              ),\n            },\n          },\n        ],\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/repo/index.tsx",
    "content": "import { useInvalidate, useRead, useWrite } from \"@lib/hooks\";\nimport { RequiredResourceComponents } from \"@types\";\nimport { Card } from \"@ui/card\";\nimport { GitBranch, Loader2, RefreshCcw } from \"lucide-react\";\nimport { RepoConfig } from \"./config\";\nimport { BuildRepo, CloneRepo, PullRepo } from \"./actions\";\nimport {\n  DeleteResource,\n  NewResource,\n  ResourceLink,\n  ResourcePageHeader,\n} from \"../common\";\nimport { RepoTable } from \"./table\";\nimport {\n  repo_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\nimport { cn } from \"@lib/utils\";\nimport { useServer } from \"../server\";\nimport { Types } from \"komodo_client\";\nimport { DashboardPieChart } from \"@pages/home/dashboard\";\nimport { RepoLink, StatusBadge } from \"@components/util\";\nimport { Badge } from \"@ui/badge\";\nimport { useToast } from \"@ui/use-toast\";\nimport { Button } from \"@ui/button\";\nimport { useBuilder } from \"../builder\";\nimport { GroupActions } from \"@components/group-actions\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@ui/tooltip\";\n\nexport const useRepo = (id?: string) =>\n  useRead(\"ListRepos\", {}, { refetchInterval: 10_000 }).data?.find(\n    (d) => d.id === id\n  );\n\nexport const useFullRepo = (id: string) =>\n  useRead(\"GetRepo\", { repo: id }, { refetchInterval: 10_000 }).data;\n\nconst RepoIcon = ({ id, size }: { id?: string; size: number }) => {\n  const state = useRepo(id)?.info.state;\n  const color = stroke_color_class_by_intention(repo_state_intention(state));\n  return <GitBranch className={cn(`w-${size} h-${size}`, state && color)} />;\n};\n\nexport const RepoComponents: RequiredResourceComponents = {\n  list_item: (id) => useRepo(id),\n  resource_links: (resource) => (resource.config as Types.RepoConfig).links,\n\n  Description: () => <>Build using custom scripts. Or anything else.</>,\n\n  Dashboard: () => {\n    const summary = useRead(\"GetReposSummary\", {}).data;\n    return (\n      <DashboardPieChart\n        data={[\n          { intention: \"Good\", value: summary?.ok ?? 0, title: \"Ok\" },\n          {\n            intention: \"Warning\",\n            value: (summary?.cloning ?? 0) + (summary?.pulling ?? 0),\n            title: \"Pulling\",\n          },\n          {\n            intention: \"Critical\",\n            value: summary?.failed ?? 0,\n            title: \"Failed\",\n          },\n          {\n            intention: \"Unknown\",\n            value: summary?.unknown ?? 0,\n            title: \"Unknown\",\n          },\n        ]}\n      />\n    );\n  },\n\n  New: ({ server_id }) => <NewResource type=\"Repo\" server_id={server_id} />,\n\n  GroupActions: () => (\n    <GroupActions\n      type=\"Repo\"\n      actions={[\"PullRepo\", \"CloneRepo\", \"BuildRepo\"]}\n    />\n  ),\n\n  Table: ({ resources }) => (\n    <RepoTable repos={resources as Types.RepoListItem[]} />\n  ),\n\n  Icon: ({ id }) => <RepoIcon id={id} size={4} />,\n  BigIcon: ({ id }) => <RepoIcon id={id} size={8} />,\n\n  State: ({ id }) => {\n    const state = useRepo(id)?.info.state;\n    return <StatusBadge text={state} intent={repo_state_intention(state)} />;\n  },\n\n  Info: {\n    Target: ({ id }) => {\n      const info = useRepo(id)?.info;\n      const server = useServer(info?.server_id);\n      const builder = useBuilder(info?.builder_id);\n      return (\n        <div className=\"flex items-center gap-x-4 gap-y-2 flex-wrap\">\n          {server?.id &&\n            (builder?.id ? (\n              <div className=\"pr-4 text-sm border-r\">\n                <ResourceLink type=\"Server\" id={server.id} />\n              </div>\n            ) : (\n              <ResourceLink type=\"Server\" id={server.id} />\n            ))}\n          {builder?.id && <ResourceLink type=\"Builder\" id={builder.id} />}\n        </div>\n      );\n    },\n    Source: ({ id }) => {\n      const info = useRepo(id)?.info;\n      if (!info) {\n        return <Loader2 className=\"w-4 h-4 animate-spin\" />;\n      }\n      return <RepoLink link={info.repo_link} repo={info.repo} />;\n    },\n    Branch: ({ id }) => {\n      const branch = useRepo(id)?.info.branch;\n      return (\n        <div className=\"flex items-center gap-2\">\n          <GitBranch className=\"w-4 h-4\" />\n          {branch}\n        </div>\n      );\n    },\n  },\n\n  Status: {\n    Cloned: ({ id }) => {\n      const info = useRepo(id)?.info;\n      if (!info?.cloned_hash || info.cloned_hash === info.latest_hash) {\n        return null;\n      }\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card className=\"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer\">\n              <div className=\"text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                cloned: {info.cloned_hash}\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent>\n            <div className=\"grid\">\n              <div className=\"text-muted-foreground\">commit message:</div>\n              {info.cloned_message}\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n    Built: ({ id }) => {\n      const info = useRepo(id)?.info;\n      const fullInfo = useFullRepo(id)?.info;\n      if (!info?.built_hash || info.built_hash === info.latest_hash) {\n        return null;\n      }\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card className=\"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer\">\n              <div className=\"text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                built: {info.built_hash}\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent>\n            <div className=\"grid gap-2\">\n              <Badge\n                variant=\"secondary\"\n                className=\"w-fit text-muted-foreground\"\n              >\n                commit message\n              </Badge>\n              {fullInfo?.built_message}\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n    Latest: ({ id }) => {\n      const info = useRepo(id)?.info;\n      const fullInfo = useFullRepo(id)?.info;\n      if (!info?.latest_hash) {\n        return null;\n      }\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card className=\"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer\">\n              <div className=\"text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                latest: {info.latest_hash}\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent>\n            <div className=\"grid gap-2\">\n              <Badge\n                variant=\"secondary\"\n                className=\"w-fit text-muted-foreground\"\n              >\n                commit message\n              </Badge>\n              {fullInfo?.latest_message}\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n    Refresh: ({ id }) => {\n      const { toast } = useToast();\n      const inv = useInvalidate();\n      const { mutate, isPending } = useWrite(\"RefreshRepoCache\", {\n        onSuccess: () => {\n          inv([\"ListRepos\"], [\"GetRepo\", { repo: id }]);\n          toast({ title: \"Refreshed repo status cache\" });\n        },\n      });\n      return (\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          onClick={() => {\n            mutate({ repo: id });\n            toast({ title: \"Triggered refresh of repo status cache\" });\n          }}\n        >\n          {isPending ? (\n            <Loader2 className=\"w-4 h-4 animate-spin\" />\n          ) : (\n            <RefreshCcw className=\"w-4 h-4\" />\n          )}\n        </Button>\n      );\n    },\n  },\n\n  Actions: { BuildRepo, PullRepo, CloneRepo },\n\n  Page: {},\n\n  Config: RepoConfig,\n\n  DangerZone: ({ id }) => <DeleteResource type=\"Repo\" id={id} />,\n\n  ResourcePageHeader: ({ id }) => {\n    const repo = useRepo(id);\n\n    return (\n      <ResourcePageHeader\n        intent={repo_state_intention(repo?.info.state)}\n        icon={<RepoIcon id={id} size={8} />}\n        type=\"Repo\"\n        id={id}\n        resource={repo}\n        state={repo?.info.state}\n        status=\"\"\n      />\n    );\n  },\n};\n"
  },
  {
    "path": "frontend/src/components/resources/repo/table.tsx",
    "content": "import { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { ResourceLink } from \"../common\";\nimport { TableTags } from \"@components/tags\";\nimport { RepoComponents } from \".\";\nimport { Types } from \"komodo_client\";\nimport { useSelectedResources } from \"@lib/hooks\";\nimport { RepoLink } from \"@components/util\";\n\nexport const RepoTable = ({ repos }: { repos: Types.RepoListItem[] }) => {\n  const [_, setSelectedResources] = useSelectedResources(\"Repo\");\n\n  return (\n    <DataTable\n      tableKey=\"repos\"\n      data={repos}\n      selectOptions={{\n        selectKey: ({ name }) => name,\n        onSelect: setSelectedResources,\n      }}\n      columns={[\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Name\" />\n          ),\n          accessorKey: \"name\",\n          cell: ({ row }) => <ResourceLink type=\"Repo\" id={row.original.id} />,\n          size: 200,\n        },\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Repo\" />\n          ),\n          accessorKey: \"info.repo\",\n          cell: ({ row }) => (\n            <RepoLink\n              repo={row.original.info.repo}\n              link={row.original.info.repo_link}\n            />\n          ),\n          size: 200,\n        },\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Branch\" />\n          ),\n          accessorKey: \"info.branch\",\n          size: 200,\n        },\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"State\" />\n          ),\n          accessorKey: \"info.state\",\n          cell: ({ row }) => <RepoComponents.State id={row.original.id} />,\n          size: 120,\n        },\n        {\n          header: \"Tags\",\n          cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/resource-sync/actions.tsx",
    "content": "import {\n  ActionButton,\n  ActionWithDialog,\n  ConfirmButton,\n} from \"@components/util\";\nimport { useExecute, useInvalidate, useRead, useWrite } from \"@lib/hooks\";\nimport { file_contents_empty, sync_no_changes } from \"@lib/utils\";\nimport { usePermissions } from \"@lib/hooks\";\nimport { NotebookPen, RefreshCcw, SquarePlay } from \"lucide-react\";\nimport { useFullResourceSync, useResourceSyncTabsView } from \".\";\n\nexport const RefreshSync = ({ id }: { id: string }) => {\n  const inv = useInvalidate();\n  const { mutate, isPending } = useWrite(\"RefreshResourceSyncPending\", {\n    onSuccess: () => inv([\"GetResourceSync\"], [\"ListResourceSyncs\"]),\n  });\n  const pending = isPending;\n  return (\n    <ActionButton\n      title=\"Refresh\"\n      icon={<RefreshCcw className=\"w-4 h-4\" />}\n      onClick={() => mutate({ sync: id })}\n      disabled={pending}\n      loading={pending}\n    />\n  );\n};\n\nexport const ExecuteSync = ({ id }: { id: string }) => {\n  const { mutate, isPending } = useExecute(\"RunSync\");\n  const syncing = useRead(\n    \"GetResourceSyncActionState\",\n    { sync: id },\n    { refetchInterval: 5000 }\n  ).data?.syncing;\n  const sync = useFullResourceSync(id);\n  const { view } = useResourceSyncTabsView(sync);\n\n  if (\n    view !== \"Execute\" ||\n    !sync ||\n    sync_no_changes(sync) ||\n    !sync.info?.remote_contents\n  ) {\n    return null;\n  }\n\n  let all_empty = true;\n  for (const contents of sync.info.remote_contents) {\n    if (contents.contents.length > 0) {\n      all_empty = false;\n      break;\n    }\n  }\n\n  if (all_empty) return null;\n\n  const pending = isPending || syncing;\n\n  return (\n    <ActionWithDialog\n      name={sync.name}\n      title=\"Execute Sync\"\n      icon={<SquarePlay className=\"w-4 h-4\" />}\n      onClick={() => mutate({ sync: id })}\n      disabled={pending}\n      loading={pending}\n    />\n  );\n};\n\nexport const CommitSync = ({ id }: { id: string }) => {\n  const { mutate, isPending } = useWrite(\"CommitSync\");\n  const sync = useFullResourceSync(id);\n  const { view } = useResourceSyncTabsView(sync);\n  const { canWrite } = usePermissions({ type: \"ResourceSync\", id });\n\n  if (view !== \"Commit\" || !canWrite || !sync) {\n    return null;\n  }\n\n  const freshSync =\n    !sync.config?.files_on_host &&\n    file_contents_empty(sync.config?.file_contents) &&\n    !sync.config?.repo &&\n    !sync.config?.linked_repo;\n\n  if (!freshSync && (!sync.config?.managed || sync_no_changes(sync))) {\n    return null;\n  }\n\n  if (freshSync) {\n    return (\n      <ConfirmButton\n        title=\"Commit Changes\"\n        icon={<NotebookPen className=\"w-4 h-4\" />}\n        onClick={() => mutate({ sync: id })}\n        disabled={isPending}\n        loading={isPending}\n      />\n    );\n  } else {\n    return (\n      <ActionWithDialog\n        name={sync.name}\n        title=\"Commit Changes\"\n        icon={<NotebookPen className=\"w-4 h-4\" />}\n        onClick={() => mutate({ sync: id })}\n        disabled={isPending}\n        loading={isPending}\n      />\n    );\n  }\n};\n"
  },
  {
    "path": "frontend/src/components/resources/resource-sync/config.tsx",
    "content": "import { Config, ConfigComponent } from \"@components/config\";\nimport {\n  AccountSelectorConfig,\n  ConfigItem,\n  ConfigList,\n  ConfigSwitch,\n  ProviderSelectorConfig,\n  WebhookBuilder,\n} from \"@components/config/util\";\nimport {\n  getWebhookIntegration,\n  useInvalidate,\n  useLocalStorage,\n  usePermissions,\n  useRead,\n  useWebhookIdOrName,\n  useWebhookIntegrations,\n  useWrite,\n} from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { ReactNode, useState } from \"react\";\nimport { CopyWebhook } from \"../common\";\nimport { useToast } from \"@ui/use-toast\";\nimport { text_color_class_by_intention } from \"@lib/color\";\nimport { ConfirmButton, ShowHideButton } from \"@components/util\";\nimport { Ban, CirclePlus, MinusCircle, SearchX, Tag } from \"lucide-react\";\nimport { MonacoEditor } from \"@components/monaco\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { filterBySplit } from \"@lib/utils\";\nimport { Button } from \"@ui/button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport { LinkedRepoConfig } from \"@components/config/linked_repo\";\n\ntype SyncMode = \"UI Defined\" | \"Files On Server\" | \"Git Repo\" | undefined;\nconst SYNC_MODES: SyncMode[] = [\"UI Defined\", \"Files On Server\", \"Git Repo\"];\n\nfunction getSyncMode(\n  update: Partial<Types.ResourceSyncConfig>,\n  config: Types.ResourceSyncConfig\n): SyncMode {\n  if (update.files_on_host ?? config.files_on_host) return \"Files On Server\";\n  if (\n    (update.repo ?? config.repo) ||\n    (update.linked_repo ?? config.linked_repo)\n  )\n    return \"Git Repo\";\n  if (update.file_contents ?? config.file_contents) return \"UI Defined\";\n  return undefined;\n}\n\nexport const ResourceSyncConfig = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const [show, setShow] = useLocalStorage(`sync-${id}-show`, {\n    file: true,\n    git: true,\n    webhooks: true,\n  });\n  const { canWrite } = usePermissions({ type: \"ResourceSync\", id });\n  const sync = useRead(\"GetResourceSync\", { sync: id }).data;\n  const config = sync?.config;\n  const name = sync?.name;\n  const webhooks = useRead(\"GetSyncWebhooksEnabled\", { sync: id }).data;\n  const global_disabled =\n    useRead(\"GetCoreInfo\", {}).data?.ui_write_disabled ?? false;\n  const [update, set] = useLocalStorage<Partial<Types.ResourceSyncConfig>>(\n    `sync-${id}-update-v1`,\n    {}\n  );\n  const { mutateAsync } = useWrite(\"UpdateResourceSync\");\n  const { integrations } = useWebhookIntegrations();\n  const [id_or_name] = useWebhookIdOrName();\n\n  if (!config) return null;\n\n  const disabled = global_disabled || !canWrite;\n\n  const git_provider = update.git_provider ?? config.git_provider;\n  const webhook_integration = getWebhookIntegration(integrations, git_provider);\n\n  const mode = getSyncMode(update, config);\n  const managed = update.managed ?? config.managed ?? false;\n\n  const setMode = (mode: SyncMode) => {\n    if (mode === \"Files On Server\") {\n      set({ ...update, files_on_host: true });\n    } else if (mode === \"Git Repo\") {\n      set({\n        ...update,\n        files_on_host: false,\n        repo: update.repo || config.repo || \"namespace/repo\",\n      });\n    } else if (mode === \"UI Defined\") {\n      set({\n        ...update,\n        files_on_host: false,\n        repo: \"\",\n        file_contents:\n          update.file_contents ||\n          config.file_contents ||\n          \"# Initialize the sync to import your current resources.\\n\",\n      });\n    } else if (mode === undefined) {\n      set({\n        ...update,\n        files_on_host: false,\n        repo: \"\",\n        file_contents: \"\",\n      });\n    }\n  };\n\n  let components: Record<\n    string,\n    false | ConfigComponent<Types.ResourceSyncConfig>[] | undefined\n  > = {};\n\n  const choose_mode: ConfigComponent<Types.ResourceSyncConfig> = {\n    label: \"Choose Mode\",\n    labelHidden: true,\n    components: {\n      file_contents: () => {\n        return (\n          <ConfigItem\n            label=\"Choose Mode\"\n            description=\"Will the file contents be defined in UI, stored on the server, or pulled from a git repo?\"\n            boldLabel\n          >\n            <Select\n              value={mode}\n              onValueChange={(mode) => setMode(mode as SyncMode)}\n              disabled={disabled}\n            >\n              <SelectTrigger\n                className=\"w-[200px] capitalize\"\n                disabled={disabled}\n              >\n                <SelectValue placeholder=\"Select Mode\" />\n              </SelectTrigger>\n              <SelectContent>\n                {SYNC_MODES.map((mode) => (\n                  <SelectItem\n                    key={mode}\n                    value={mode!}\n                    className=\"capitalize cursor-pointer\"\n                  >\n                    {mode}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </ConfigItem>\n        );\n      },\n    },\n  };\n\n  const general_common: ConfigComponent<Types.ResourceSyncConfig> = {\n    label: \"General\",\n    components: {\n      delete: (delete_mode, set) => {\n        return (\n          <ConfigSwitch\n            label=\"Delete Unmatched Resources\"\n            description=\"Executions will delete any resources not found in the resource files. Only use this when using one sync for everything.\"\n            value={managed || delete_mode}\n            onChange={(delete_mode) => set({ delete: delete_mode })}\n            disabled={disabled || managed}\n          />\n        );\n      },\n      managed: {\n        label: \"Managed\",\n        description:\n          \"Enabled managed mode / the 'Commit' button. Commit is the 'reverse' of Execute, and will update the sync file with your configs updated in the UI.\",\n      },\n    },\n  };\n\n  const include_toggles: ConfigComponent<Types.ResourceSyncConfig> = {\n    label: \"Include\",\n    components: {\n      include_resources: {\n        label: \"Sync Resources\",\n        description: \"Include resources (servers, stacks, etc.) in the sync.\",\n      },\n      include_variables: {\n        label: \"Sync Variables\",\n        description: \"Include variables in the sync.\",\n      },\n      include_user_groups: {\n        label: \"Sync User Groups\",\n        description: \"Include user groups in the sync.\",\n      },\n    },\n  };\n\n  const include_resources =\n    update.include_resources ?? config.include_resources;\n  const match_tags: ConfigComponent<Types.ResourceSyncConfig> = {\n    label: \"Match Tags\",\n    description: \"Only sync resources matching all of these tags.\",\n    components: {\n      match_tags: (values, set) => (\n        <MatchTags\n          tags={values ?? []}\n          set={set}\n          disabled={disabled || !include_resources}\n        />\n      ),\n    },\n  };\n\n  const pending_alerts: ConfigComponent<Types.ResourceSyncConfig> = {\n    label: \"Alerts\",\n    components: {\n      pending_alert: {\n        label: \"Pending Alerts\",\n        description:\n          \"Send a message to your Alerters when the Sync has Pending Changes\",\n      },\n    },\n  };\n\n  if (mode === undefined) {\n    components = {\n      \"\": [choose_mode],\n    };\n  } else if (mode === \"Files On Server\") {\n    components = {\n      \"\": [\n        {\n          label: \"General\",\n          components: {\n            resource_path: (values, set) => (\n              <ConfigList\n                label=\"Resource Paths\"\n                addLabel=\"Add Path\"\n                description=\"Add '.toml' files or folders to the sync. Relative to '/syncs/{sync_name}'.\"\n                field=\"resource_path\"\n                values={values ?? []}\n                set={set}\n                disabled={disabled}\n                placeholder=\"Input resource path\"\n              />\n            ),\n            ...general_common.components,\n          },\n        },\n        match_tags,\n        include_toggles,\n        pending_alerts,\n      ],\n    };\n  } else if (mode === \"Git Repo\") {\n    const repo_linked = !!(update.linked_repo ?? config.linked_repo);\n    const source_config: ConfigComponent<Types.ResourceSyncConfig> = {\n      label: \"Source\",\n      contentHidden: !show.git,\n      actions: (\n        <ShowHideButton\n          show={show.git}\n          setShow={(git) => setShow({ ...show, git })}\n        />\n      ),\n      components: {\n        linked_repo: (linked_repo, set) => (\n          <LinkedRepoConfig\n            linked_repo={linked_repo}\n            repo_linked={repo_linked}\n            set={set}\n            disabled={disabled}\n          />\n        ),\n        ...(!repo_linked\n          ? {\n              git_provider: (provider: string | undefined, set) => {\n                const https = update.git_https ?? config.git_https;\n                return (\n                  <ProviderSelectorConfig\n                    account_type=\"git\"\n                    selected={provider}\n                    disabled={disabled}\n                    onSelect={(git_provider) => set({ git_provider })}\n                    https={https}\n                    onHttpsSwitch={() => set({ git_https: !https })}\n                  />\n                );\n              },\n              git_account: (value: string | undefined, set) => {\n                return (\n                  <AccountSelectorConfig\n                    account_type=\"git\"\n                    type=\"None\"\n                    provider={update.git_provider ?? config.git_provider}\n                    selected={value}\n                    onSelect={(git_account) => set({ git_account })}\n                    disabled={disabled}\n                    placeholder=\"None\"\n                  />\n                );\n              },\n              repo: {\n                placeholder: \"Enter repo\",\n                description:\n                  \"The repo path on the provider. {namespace}/{repo_name}\",\n              },\n              branch: {\n                placeholder: \"Enter branch\",\n                description: \"Select a custom branch, or default to 'main'.\",\n              },\n              commit: {\n                label: \"Commit Hash\",\n                placeholder: \"Input commit hash\",\n                description:\n                  \"Optional. Switch to a specific commit hash after cloning the branch.\",\n              },\n            }\n          : {}),\n      },\n    };\n    const webhooks_config: ConfigComponent<Types.ResourceSyncConfig> = {\n      label: \"Git Webhooks\",\n      description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,\n      contentHidden: !show.webhooks,\n      actions: (\n        <ShowHideButton\n          show={show.webhooks}\n          setShow={(webhooks) => setShow({ ...show, webhooks })}\n        />\n      ),\n      components: {\n        [\"Guard\" as any]: () => {\n          if (update.branch ?? config.branch) {\n            return null;\n          }\n          return (\n            <ConfigItem label=\"Configure Branch\">\n              <div>Must configure Branch before webhooks will work.</div>\n            </ConfigItem>\n          );\n        },\n        [\"Builder\" as any]: () => (\n          <WebhookBuilder git_provider={git_provider} />\n        ),\n        [\"Refresh\" as any]: () => (\n          <ConfigItem\n            label=\"Webhook Url - Refresh Pending\"\n            description=\"Trigger an update of the pending sync cache, to display the changes in the UI on push.\"\n          >\n            <CopyWebhook\n              integration={webhook_integration}\n              path={`/sync/${id_or_name === \"Id\" ? id : encodeURIComponent(name ?? \"...\")}/refresh`}\n            />\n          </ConfigItem>\n        ),\n        [\"Sync\" as any]: () => (\n          <ConfigItem\n            label=\"Webhook Url - Execute Sync\"\n            description=\"Trigger an execution of the sync on push.\"\n          >\n            <CopyWebhook\n              integration={webhook_integration}\n              path={`/sync/${id_or_name === \"Id\" ? id : encodeURIComponent(name ?? \"...\")}/sync`}\n            />\n          </ConfigItem>\n        ),\n        webhook_enabled: webhooks !== undefined && !webhooks.managed,\n        webhook_secret: {\n          description:\n            \"Provide a custom webhook secret for this resource, or use the global default.\",\n          placeholder: \"Input custom secret\",\n        },\n        [\"managed\" as any]: () => {\n          const inv = useInvalidate();\n          const { toast } = useToast();\n          const { mutate: createWebhook, isPending: createPending } = useWrite(\n            \"CreateSyncWebhook\",\n            {\n              onSuccess: () => {\n                toast({ title: \"Webhook Created\" });\n                inv([\"GetSyncWebhooksEnabled\", { sync: id }]);\n              },\n            }\n          );\n          const { mutate: deleteWebhook, isPending: deletePending } = useWrite(\n            \"DeleteSyncWebhook\",\n            {\n              onSuccess: () => {\n                toast({ title: \"Webhook Deleted\" });\n                inv([\"GetSyncWebhooksEnabled\", { sync: id }]);\n              },\n            }\n          );\n          if (!webhooks || !webhooks.managed) return;\n          return (\n            <ConfigItem label=\"Manage Webhook\">\n              {webhooks.sync_enabled && (\n                <div className=\"flex items-center gap-4 flex-wrap\">\n                  <div className=\"flex items-center gap-2\">\n                    Incoming webhook is{\" \"}\n                    <div className={text_color_class_by_intention(\"Good\")}>\n                      ENABLED\n                    </div>\n                    and will trigger\n                    <div className={text_color_class_by_intention(\"Neutral\")}>\n                      SYNC EXECUTION\n                    </div>\n                  </div>\n                  <ConfirmButton\n                    title=\"Disable\"\n                    icon={<Ban className=\"w-4 h-4\" />}\n                    variant=\"destructive\"\n                    onClick={() =>\n                      deleteWebhook({\n                        sync: id,\n                        action: Types.SyncWebhookAction.Sync,\n                      })\n                    }\n                    loading={deletePending}\n                    disabled={disabled || deletePending}\n                  />\n                </div>\n              )}\n              {!webhooks.sync_enabled && webhooks.refresh_enabled && (\n                <div className=\"flex items-center gap-4 flex-wrap\">\n                  <div className=\"flex items-center gap-2\">\n                    Incoming webhook is{\" \"}\n                    <div className={text_color_class_by_intention(\"Good\")}>\n                      ENABLED\n                    </div>\n                    and will trigger\n                    <div className={text_color_class_by_intention(\"Neutral\")}>\n                      PENDING REFRESH\n                    </div>\n                  </div>\n                  <ConfirmButton\n                    title=\"Disable\"\n                    icon={<Ban className=\"w-4 h-4\" />}\n                    variant=\"destructive\"\n                    onClick={() =>\n                      deleteWebhook({\n                        sync: id,\n                        action: Types.SyncWebhookAction.Refresh,\n                      })\n                    }\n                    loading={deletePending}\n                    disabled={disabled || deletePending}\n                  />\n                </div>\n              )}\n              {!webhooks.sync_enabled && !webhooks.refresh_enabled && (\n                <div className=\"flex items-center gap-4 flex-wrap\">\n                  <div className=\"flex items-center gap-2\">\n                    Incoming webhook is{\" \"}\n                    <div className={text_color_class_by_intention(\"Critical\")}>\n                      DISABLED\n                    </div>\n                  </div>\n                  <ConfirmButton\n                    title=\"Enable Refresh\"\n                    icon={<CirclePlus className=\"w-4 h-4\" />}\n                    onClick={() =>\n                      createWebhook({\n                        sync: id,\n                        action: Types.SyncWebhookAction.Refresh,\n                      })\n                    }\n                    loading={createPending}\n                    disabled={disabled || createPending}\n                  />\n                  <ConfirmButton\n                    title=\"Enable Sync\"\n                    icon={<CirclePlus className=\"w-4 h-4\" />}\n                    onClick={() =>\n                      createWebhook({\n                        sync: id,\n                        action: Types.SyncWebhookAction.Sync,\n                      })\n                    }\n                    loading={createPending}\n                    disabled={disabled || createPending}\n                  />\n                </div>\n              )}\n            </ConfigItem>\n          );\n        },\n      },\n    };\n    components = {\n      \"\": [\n        source_config,\n        {\n          label: \"General\",\n          components: {\n            resource_path: (values, set) => (\n              <ConfigList\n                label=\"Resource Paths\"\n                addLabel=\"Add Path\"\n                description=\"Add '.toml' files or folders to the sync. Relative to the root of the repo.\"\n                field=\"resource_path\"\n                values={values ?? []}\n                set={set}\n                disabled={disabled}\n                placeholder=\"Input resource path\"\n              />\n            ),\n            ...general_common.components,\n          },\n        },\n        match_tags,\n        include_toggles,\n        pending_alerts,\n        webhooks_config,\n      ],\n    };\n  } else if (mode === \"UI Defined\") {\n    components = {\n      \"\": [\n        {\n          label: \"Resource File\",\n          description:\n            \"Manage the resource file contents here, or use a git repo / the files on host option.\",\n          actions: (\n            <ShowHideButton\n              show={show.file}\n              setShow={(file) => setShow((show) => ({ ...show, file }))}\n            />\n          ),\n          contentHidden: !show.file,\n          components: {\n            file_contents: (file_contents, set) => {\n              return (\n                <MonacoEditor\n                  value={\n                    file_contents ||\n                    \"# Initialize the sync to import your current resources.\\n\"\n                  }\n                  onValueChange={(file_contents) => set({ file_contents })}\n                  language=\"fancy_toml\"\n                  readOnly={disabled}\n                />\n              );\n            },\n          },\n        },\n        general_common,\n        match_tags,\n        include_toggles,\n        pending_alerts,\n      ],\n    };\n  }\n\n  return (\n    <Config\n      titleOther={titleOther}\n      disabled={disabled}\n      original={config}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: update });\n      }}\n      components={components}\n      file_contents_language=\"fancy_toml\"\n    />\n  );\n};\n\nconst MatchTags = ({\n  tags,\n  set,\n  disabled,\n}: {\n  tags: string[];\n  set: (update: Partial<Types.ResourceSyncConfig>) => void;\n  disabled: boolean;\n}) => {\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const all_tags = useRead(\"ListTags\", {}).data;\n  const filtered = filterBySplit(all_tags, search, (item) => item.name);\n  return (\n    <div className=\"flex gap-3 items-center\">\n      <Popover\n        open={open}\n        onOpenChange={(open) => {\n          setSearch(\"\");\n          setOpen(open);\n        }}\n      >\n        <PopoverTrigger asChild>\n          <Button\n            variant=\"outline\"\n            className=\"flex items-center gap-2\"\n            disabled={disabled}\n          >\n            <Tag className=\"w-3 h-3\" />\n            Select Tag\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent\n          className=\"w-[200px] max-h-[200px] p-0\"\n          sideOffset={12}\n          align=\"start\"\n        >\n          <Command shouldFilter={false}>\n            <CommandInput\n              placeholder=\"Search Tags\"\n              className=\"h-9\"\n              value={search}\n              onValueChange={setSearch}\n            />\n            <CommandList>\n              <CommandEmpty className=\"flex justify-evenly items-center pt-2\">\n                No Tags Found\n                <SearchX className=\"w-3 h-3\" />\n              </CommandEmpty>\n\n              <CommandGroup>\n                {filtered\n                  ?.filter((tag) => !tags.includes(tag.name))\n                  .map((tag) => (\n                    <CommandItem\n                      key={tag.name}\n                      onSelect={() => {\n                        set({ match_tags: [...tags, tag.name] });\n                        setSearch(\"\");\n                        setOpen(false);\n                      }}\n                      className=\"flex items-center justify-between cursor-pointer\"\n                    >\n                      <div className=\"p-1\">{tag.name}</div>\n                    </CommandItem>\n                  ))}\n              </CommandGroup>\n            </CommandList>\n          </Command>\n        </PopoverContent>\n      </Popover>\n\n      <MatchTagsTags\n        tags={tags}\n        onBadgeClick={(tag) =>\n          set({ match_tags: tags.filter((name) => name !== tag) })\n        }\n        disabled={disabled}\n      />\n    </div>\n  );\n};\n\nconst MatchTagsTags = ({\n  tags,\n  onBadgeClick,\n  disabled,\n}: {\n  tags?: string[];\n  onBadgeClick: (tag: string) => void;\n  disabled: boolean;\n}) => {\n  return (\n    <>\n      {tags?.map((tag) => (\n        <Button\n          key={tag}\n          variant=\"secondary\"\n          className=\"flex items-center gap-2\"\n          onClick={() => onBadgeClick && onBadgeClick(tag)}\n          disabled={disabled}\n        >\n          {tag}\n          <MinusCircle className=\"w-4 h-4\" />\n        </Button>\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/resource-sync/index.tsx",
    "content": "import { atomWithStorage, useRead, useUser } from \"@lib/hooks\";\nimport { RequiredResourceComponents } from \"@types\";\nimport { Card } from \"@ui/card\";\nimport { Clock, FolderSync } from \"lucide-react\";\nimport {\n  DeleteResource,\n  NewResource,\n  ResourcePageHeader,\n  StandardSource,\n} from \"../common\";\nimport { ResourceSyncTable } from \"./table\";\nimport { Types } from \"komodo_client\";\nimport { CommitSync, ExecuteSync, RefreshSync } from \"./actions\";\nimport {\n  border_color_class_by_intention,\n  resource_sync_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\nimport { cn, sync_no_changes } from \"@lib/utils\";\nimport { fmt_date } from \"@lib/formatting\";\nimport { DashboardPieChart } from \"@pages/home/dashboard\";\nimport { StatusBadge } from \"@components/util\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@ui/tabs\";\nimport { ResourceSyncConfig } from \"./config\";\nimport { ResourceSyncInfo } from \"./info\";\nimport { ResourceSyncPending } from \"./pending\";\nimport { Badge } from \"@ui/badge\";\nimport { GroupActions } from \"@components/group-actions\";\nimport { useAtom } from \"jotai\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@ui/tooltip\";\n\nexport const useResourceSync = (id?: string) =>\n  useRead(\"ListResourceSyncs\", {}, { refetchInterval: 10_000 }).data?.find(\n    (d) => d.id === id\n  );\n\nexport const useFullResourceSync = (id: string) =>\n  useRead(\"GetResourceSync\", { sync: id }, { refetchInterval: 10_000 }).data;\n\nconst ResourceSyncIcon = ({ id, size }: { id?: string; size: number }) => {\n  const state = useResourceSync(id)?.info.state;\n  const color = stroke_color_class_by_intention(\n    resource_sync_state_intention(state)\n  );\n  return <FolderSync className={cn(`w-${size} h-${size}`, state && color)} />;\n};\n\ntype ResourceSyncTabsView = \"Config\" | \"Info\" | \"Execute\" | \"Commit\";\nconst syncTabsViewAtom = atomWithStorage<ResourceSyncTabsView>(\n  \"sync-tabs-v4\",\n  \"Config\"\n);\n\nexport const useResourceSyncTabsView = (\n  sync: Types.ResourceSync | undefined\n) => {\n  const [_view, setView] = useAtom<ResourceSyncTabsView>(syncTabsViewAtom);\n\n  const hideInfo = sync?.config?.files_on_host\n    ? false\n    : sync?.config?.file_contents\n      ? true\n      : false;\n\n  const showPending =\n    sync && (!sync_no_changes(sync) || sync.info?.pending_error);\n\n  const view =\n    _view === \"Info\" && hideInfo\n      ? \"Config\"\n      : (_view === \"Execute\" || _view === \"Commit\") && !showPending\n        ? sync?.config?.files_on_host ||\n          sync?.config?.repo ||\n          sync?.config?.linked_repo\n          ? \"Info\"\n          : \"Config\"\n        : _view === \"Commit\" && !sync?.config?.managed\n          ? \"Execute\"\n          : _view;\n\n  return {\n    view,\n    setView,\n    hideInfo,\n    showPending,\n  };\n};\n\nconst ConfigInfoPending = ({ id }: { id: string }) => {\n  const sync = useFullResourceSync(id);\n  const { view, setView, hideInfo, showPending } =\n    useResourceSyncTabsView(sync);\n\n  const title = (\n    <TabsList className=\"justify-start w-fit\">\n      <TabsTrigger value=\"Config\" className=\"w-[110px]\">\n        Config\n      </TabsTrigger>\n      <TabsTrigger\n        value=\"Info\"\n        className={cn(\"w-[110px]\", hideInfo && \"hidden\")}\n        disabled={hideInfo}\n      >\n        Info\n      </TabsTrigger>\n      <TabsTrigger\n        value=\"Execute\"\n        className=\"w-[110px]\"\n        disabled={!showPending}\n      >\n        Execute\n      </TabsTrigger>\n      {sync?.config?.managed && (\n        <TabsTrigger\n          value=\"Commit\"\n          className=\"w-[110px]\"\n          disabled={!showPending}\n        >\n          Commit\n        </TabsTrigger>\n      )}\n    </TabsList>\n  );\n  return (\n    <Tabs value={view} onValueChange={setView as any}>\n      <TabsContent value=\"Config\">\n        <ResourceSyncConfig id={id} titleOther={title} />\n      </TabsContent>\n      <TabsContent value=\"Info\">\n        <ResourceSyncInfo id={id} titleOther={title} />\n      </TabsContent>\n      <TabsContent value=\"Execute\">\n        <ResourceSyncPending id={id} titleOther={title} />\n      </TabsContent>\n      <TabsContent value=\"Commit\">\n        <ResourceSyncPending id={id} titleOther={title} />\n      </TabsContent>\n    </Tabs>\n  );\n};\n\nexport const ResourceSyncComponents: RequiredResourceComponents = {\n  list_item: (id) => useResourceSync(id),\n  resource_links: () => undefined,\n\n  Description: () => <>Declare resources in TOML files.</>,\n\n  Dashboard: () => {\n    const summary = useRead(\"GetResourceSyncsSummary\", {}).data;\n    return (\n      <DashboardPieChart\n        data={[\n          { title: \"Ok\", intention: \"Good\", value: summary?.ok ?? 0 },\n          {\n            title: \"Syncing\",\n            intention: \"Warning\",\n            value: summary?.syncing ?? 0,\n          },\n          {\n            title: \"Pending\",\n            intention: \"Neutral\",\n            value: summary?.pending ?? 0,\n          },\n          {\n            title: \"Failed\",\n            intention: \"Critical\",\n            value: summary?.failed ?? 0,\n          },\n          {\n            title: \"Unknown\",\n            intention: \"Unknown\",\n            value: summary?.unknown ?? 0,\n          },\n        ]}\n      />\n    );\n  },\n\n  New: () => {\n    const admin = useUser().data?.admin;\n    return (\n      admin && <NewResource type=\"ResourceSync\" readable_type=\"Resource Sync\" />\n    );\n  },\n\n  GroupActions: () => (\n    <GroupActions type=\"ResourceSync\" actions={[\"RunSync\", \"CommitSync\"]} />\n  ),\n\n  Table: ({ resources }) => (\n    <ResourceSyncTable syncs={resources as Types.ResourceSyncListItem[]} />\n  ),\n\n  Icon: ({ id }) => <ResourceSyncIcon id={id} size={4} />,\n  BigIcon: ({ id }) => <ResourceSyncIcon id={id} size={8} />,\n\n  State: ({ id }) => {\n    const state = useResourceSync(id)?.info.state;\n    return (\n      <StatusBadge text={state} intent={resource_sync_state_intention(state)} />\n    );\n  },\n\n  Info: {\n    Source: ({ id }) => {\n      const info = useResourceSync(id)?.info;\n      return <StandardSource info={info} />;\n    },\n    LastSync: ({ id }) => {\n      const last_ts = useResourceSync(id)?.info.last_sync_ts;\n      return (\n        <div className=\"flex items-center gap-2\">\n          <Clock className=\"w-4 h-4\" />\n          {last_ts ? fmt_date(new Date(last_ts)) : \"Never\"}\n        </div>\n      );\n    },\n  },\n\n  Status: {\n    Hash: ({ id }) => {\n      const info = useFullResourceSync(id)?.info;\n      if (!info?.pending_hash) {\n        return null;\n      }\n      const out_of_date =\n        info.last_sync_hash && info.last_sync_hash !== info.pending_hash;\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card\n              className={cn(\n                \"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer\",\n                out_of_date && border_color_class_by_intention(\"Warning\")\n              )}\n            >\n              <div className=\"text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                {info.last_sync_hash ? \"synced\" : \"latest\"}:{\" \"}\n                {info.last_sync_hash || info.pending_hash}\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent>\n            <div className=\"grid gap-2\">\n              <Badge\n                variant=\"secondary\"\n                className=\"w-fit text-muted-foreground\"\n              >\n                message\n              </Badge>\n              {info.last_sync_message || info.pending_message}\n              {out_of_date && (\n                <>\n                  <Badge\n                    variant=\"secondary\"\n                    className={cn(\n                      \"w-fit text-muted-foreground border-[1px]\",\n                      border_color_class_by_intention(\"Warning\")\n                    )}\n                  >\n                    latest\n                  </Badge>\n                  <div>\n                    <span className=\"text-muted-foreground\">\n                      {info.pending_hash}\n                    </span>\n                    : {info.pending_message}\n                  </div>\n                </>\n              )}\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n  },\n\n  Actions: { RefreshSync, ExecuteSync, CommitSync },\n\n  Page: {},\n\n  Config: ConfigInfoPending,\n\n  DangerZone: ({ id }) => <DeleteResource type=\"ResourceSync\" id={id} />,\n\n  ResourcePageHeader: ({ id }) => {\n    const sync = useResourceSync(id);\n\n    return (\n      <ResourcePageHeader\n        intent={resource_sync_state_intention(sync?.info.state)}\n        icon={<ResourceSyncIcon id={id} size={8} />}\n        type=\"ResourceSync\"\n        id={id}\n        resource={sync}\n        state={sync?.info.state}\n        status=\"\"\n      />\n    );\n  },\n};\n"
  },
  {
    "path": "frontend/src/components/resources/resource-sync/info.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { ReactNode, useState } from \"react\";\nimport { Card, CardContent, CardHeader } from \"@ui/card\";\nimport { useFullResourceSync } from \".\";\nimport { cn, updateLogToHtml } from \"@lib/utils\";\nimport { MonacoEditor } from \"@components/monaco\";\nimport { usePermissions } from \"@lib/hooks\";\nimport { useLocalStorage, useWrite } from \"@lib/hooks\";\nimport { useToast } from \"@ui/use-toast\";\nimport { Button } from \"@ui/button\";\nimport { FilePlus, History } from \"lucide-react\";\nimport { ConfirmUpdate } from \"@components/config/util\";\nimport { ConfirmButton, ShowHideButton } from \"@components/util\";\n\nexport const ResourceSyncInfo = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const [edits, setEdits] = useLocalStorage<Record<string, string | undefined>>(\n    `sync-${id}-edits`,\n    {}\n  );\n  const [show, setShow] = useState<Record<string, boolean | undefined>>({});\n  const { canWrite } = usePermissions({ type: \"ResourceSync\", id });\n  const { toast } = useToast();\n  const { mutateAsync, isPending } = useWrite(\"WriteSyncFileContents\", {\n    onSuccess: (res) => {\n      toast({\n        title: res.success ? \"Contents written.\" : \"Failed to write contents.\",\n        variant: res.success ? undefined : \"destructive\",\n      });\n    },\n  });\n  const sync = useFullResourceSync(id);\n  const file_on_host = sync?.config?.files_on_host ?? false;\n  const git_repo =\n    sync?.config?.repo || sync?.config?.linked_repo ? true : false;\n  const canEdit = canWrite && (file_on_host || git_repo);\n  const editFileCallback = (keyPath: string) => (contents: string) =>\n    setEdits({ ...edits, [keyPath]: contents });\n\n  const latest_contents = sync?.info?.remote_contents;\n  const latest_errors = sync?.info?.remote_errors;\n\n  // Contents will be default hidden if there is more than 2 file editor to show\n  const default_show_contents = !latest_contents || latest_contents.length < 3;\n\n  return (\n    <Section titleOther={titleOther}>\n      {/* Errors */}\n      {latest_errors &&\n        latest_errors.length > 0 &&\n        latest_errors.map((error) => (\n          <Card key={error.path} className=\"flex flex-col gap-4\">\n            <CardHeader className=\"flex flex-row justify-between items-center pb-0\">\n              <div className=\"font-mono flex gap-2\">\n                {error.resource_path && (\n                  <>\n                    <div className=\"flex gap-2\">\n                      <div className=\"text-muted-foreground\">Folder:</div>\n                      {error.resource_path}\n                    </div>\n                    <div className=\"text-muted-foreground\">|</div>\n                  </>\n                )}\n                <div className=\"flex gap-2\">\n                  <div className=\"text-muted-foreground\">Path:</div>\n                  {error.path}\n                </div>\n              </div>\n              {canEdit && (\n                <ConfirmButton\n                  title=\"Initialize File\"\n                  icon={<FilePlus className=\"w-4 h-4\" />}\n                  onClick={() => {\n                    if (sync) {\n                      mutateAsync({\n                        sync: sync.name,\n                        resource_path: error.resource_path ?? \"\",\n                        file_path: error.path,\n                        contents: \"## Add resources to get started\\n\",\n                      });\n                    }\n                  }}\n                  loading={isPending}\n                  disabled={!canEdit}\n                />\n              )}\n            </CardHeader>\n            <CardContent className=\"pr-8\">\n              <pre\n                dangerouslySetInnerHTML={{\n                  __html: updateLogToHtml(error.contents),\n                }}\n                className=\"max-h-[500px] overflow-y-auto\"\n              />\n            </CardContent>\n          </Card>\n        ))}\n\n      {/* Update latest contents */}\n      {latest_contents &&\n        latest_contents.length > 0 &&\n        latest_contents.map((content) => {\n          const keyPath = content.resource_path + \"/\" + content.path;\n          const showContents = show[keyPath] ?? default_show_contents;\n          return (\n            <Card key={keyPath} className=\"flex flex-col gap-4\">\n              <CardHeader\n                className={cn(\n                  \"flex flex-row justify-between items-center\",\n                  showContents && \"pb-2\"\n                )}\n              >\n                <div className=\"font-mono flex gap-4\">\n                  {content.resource_path && (\n                    <>\n                      <div className=\"flex gap-2\">\n                        <div className=\"text-muted-foreground\">Folder:</div>\n                        {content.resource_path}\n                      </div>\n                      <div className=\"text-muted-foreground\">|</div>\n                    </>\n                  )}\n                  <div className=\"flex gap-2\">\n                    <div className=\"text-muted-foreground\">File:</div>\n                    {content.path}\n                  </div>\n                </div>\n                <div className=\"flex items-center gap-3\">\n                  {canEdit && (\n                    <>\n                      <Button\n                        variant=\"outline\"\n                        onClick={() =>\n                          setEdits({ ...edits, [keyPath]: undefined })\n                        }\n                        className=\"flex items-center gap-2\"\n                        disabled={!edits[keyPath]}\n                      >\n                        <History className=\"w-4 h-4\" />\n                        Reset\n                      </Button>\n                      <ConfirmUpdate\n                        previous={{ contents: content.contents }}\n                        content={{ contents: edits[keyPath] }}\n                        onConfirm={async () => {\n                          if (sync) {\n                            return await mutateAsync({\n                              sync: sync.name,\n                              resource_path: content.resource_path ?? \"\",\n                              file_path: content.path,\n                              contents: edits[keyPath]!,\n                            }).then(() =>\n                              setEdits({ ...edits, [keyPath]: undefined })\n                            );\n                          }\n                        }}\n                        disabled={!edits[keyPath]}\n                        language=\"fancy_toml\"\n                        loading={isPending}\n                      />\n                    </>\n                  )}\n                  <ShowHideButton\n                    show={showContents}\n                    setShow={(val) => setShow({ ...show, [keyPath]: val })}\n                  />\n                </div>\n              </CardHeader>\n              {showContents && (\n                <CardContent className=\"pr-8\">\n                  <MonacoEditor\n                    value={edits[keyPath] ?? content.contents}\n                    language=\"fancy_toml\"\n                    readOnly={!canEdit}\n                    onValueChange={editFileCallback(keyPath)}\n                  />\n                </CardContent>\n              )}\n            </Card>\n          );\n        })}\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/resource-sync/pending.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { MonacoDiffEditor, MonacoEditor } from \"@components/monaco\";\nimport { useExecute, useRead } from \"@lib/hooks\";\nimport { Card, CardContent, CardHeader } from \"@ui/card\";\nimport { ReactNode } from \"react\";\nimport { ResourceLink } from \"../common\";\nimport { UsableResource } from \"@types\";\nimport { diff_type_intention, text_color_class_by_intention } from \"@lib/color\";\nimport { cn, sanitizeOnlySpan } from \"@lib/utils\";\nimport { ConfirmButton } from \"@components/util\";\nimport { SquarePlay } from \"lucide-react\";\nimport { usePermissions } from \"@lib/hooks\";\nimport { useFullResourceSync, useResourceSyncTabsView } from \".\";\nimport { ResourceDiff } from \"komodo_client/dist/types\";\n\nexport const ResourceSyncPending = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const syncing = useRead(\"GetResourceSyncActionState\", { sync: id }).data\n    ?.syncing;\n  const sync = useFullResourceSync(id);\n  const { view } = useResourceSyncTabsView(sync);\n  const { canExecute } = usePermissions({ type: \"ResourceSync\", id });\n  const { mutate, isPending } = useExecute(\"RunSync\");\n  const loading = isPending || syncing;\n  return (\n    <Section titleOther={titleOther} className=\"min-h-[500px]\">\n      <div className=\"flex items-center gap-4 pl-1 py-2 flex-wrap\">\n        <div className=\"text-muted-foreground\">{view} Mode:</div>\n        <div className=\"flex items-center gap-1 flex-wrap\">\n          {view === \"Execute\" && (\n            <>\n              Update resources in the\n              <div className=\"font-bold\">UI</div>\n              to match the\n              <div className=\"font-bold\">file changes.</div>\n            </>\n          )}\n          {view === \"Commit\" && (\n            <>\n              Update resources in the\n              <div className=\"font-bold\">file</div>\n              to match the\n              <div className=\"font-bold\">UI changes.</div>\n            </>\n          )}\n        </div>\n      </div>\n\n      {/* Pending Error */}\n      {sync?.info?.pending_error && sync.info.pending_error.length ? (\n        <Card>\n          <CardHeader\n            className={cn(\n              \"font-mono pb-2\",\n              text_color_class_by_intention(\"Critical\")\n            )}\n          >\n            Error\n          </CardHeader>\n          <CardContent>\n            <pre\n              dangerouslySetInnerHTML={{\n                __html: sanitizeOnlySpan(sync.info.pending_error),\n              }}\n            />\n          </CardContent>\n        </Card>\n      ) : undefined}\n\n      {/* Pending Deploy */}\n      {view === \"Execute\" && sync?.info?.pending_deploy?.to_deploy ? (\n        <Card>\n          <CardHeader\n            className={cn(\n              \"font-mono pb-2\",\n              text_color_class_by_intention(\"Warning\")\n            )}\n          >\n            Deploy {sync.info.pending_deploy.to_deploy} Resource\n            {sync.info.pending_deploy.to_deploy > 1 ? \"s\" : \"\"}\n          </CardHeader>\n          <CardContent>\n            <pre\n              dangerouslySetInnerHTML={{\n                __html: sanitizeOnlySpan(sync.info.pending_deploy.log),\n              }}\n            />\n          </CardContent>\n        </Card>\n      ) : undefined}\n\n      {/* Pending Resource Update */}\n      {sync?.info?.resource_updates?.map((update) => {\n        return (\n          <Card key={update.target.type + update.target.id}>\n            <CardHeader className=\"pb-4 flex flex-row justify-between items-center\">\n              <div className=\"flex items-center gap-4 font-mono\">\n                <div\n                  className={text_color_class_by_intention(\n                    diff_type_intention(update.data.type, view === \"Commit\")\n                  )}\n                >\n                  {view === \"Commit\"\n                    ? reverse_pending_type(update.data.type)\n                    : update.data.type}{\" \"}\n                  {update.target.type}\n                </div>\n                <div className=\"text-muted-foreground\">|</div>\n                {update.data.type === \"Create\" ? (\n                  <div>{update.data.data.name}</div>\n                ) : (\n                  <ResourceLink\n                    type={update.target.type as UsableResource}\n                    id={update.target.id}\n                  />\n                )}\n              </div>\n              {canExecute && view === \"Execute\" && (\n                <ConfirmButton\n                  title=\"Execute Change\"\n                  icon={<SquarePlay className=\"w-4 h-4\" />}\n                  onClick={() =>\n                    mutate({\n                      sync: id,\n                      resource_type: update.target.type,\n                      resources: [\n                        update.data.type === \"Create\"\n                          ? update.data.data.name!\n                          : update.target.id,\n                      ],\n                    })\n                  }\n                  loading={loading}\n                />\n              )}\n            </CardHeader>\n            <CardContent>\n              {update.data.type === \"Create\" && (\n                <MonacoEditor\n                  value={update.data.data.proposed}\n                  language=\"fancy_toml\"\n                  readOnly\n                />\n              )}\n              {update.data.type === \"Update\" && (\n                <>\n                  {view === \"Execute\" && (\n                    <MonacoDiffEditor\n                      original={update.data.data.current}\n                      modified={update.data.data.proposed}\n                      language=\"fancy_toml\"\n                      readOnly\n                    />\n                  )}\n                  {view === \"Commit\" && (\n                    <MonacoDiffEditor\n                      original={update.data.data.proposed}\n                      modified={update.data.data.current}\n                      language=\"fancy_toml\"\n                      readOnly\n                    />\n                  )}\n                </>\n              )}\n              {update.data.type === \"Delete\" && (\n                <MonacoEditor\n                  value={update.data.data.current}\n                  language=\"fancy_toml\"\n                  readOnly\n                />\n              )}\n            </CardContent>\n          </Card>\n        );\n      })}\n      {/* Pending Variable Update */}\n      {sync?.info?.variable_updates?.map((data, i) => {\n        return (\n          <Card key={i}>\n            <CardHeader\n              className={cn(\n                \"font-mono pb-2\",\n                text_color_class_by_intention(\n                  diff_type_intention(data.type, view === \"Commit\")\n                )\n              )}\n            >\n              {view === \"Commit\" ? reverse_pending_type(data.type) : data.type}{\" \"}\n              Variable\n            </CardHeader>\n            <CardContent>\n              {data.type === \"Create\" && (\n                <MonacoEditor\n                  value={data.data.proposed}\n                  language=\"fancy_toml\"\n                  readOnly\n                />\n              )}\n              {data.type === \"Update\" && (\n                <>\n                  {view === \"Execute\" && (\n                    <MonacoDiffEditor\n                      original={data.data.current}\n                      modified={data.data.proposed}\n                      language=\"fancy_toml\"\n                      readOnly\n                    />\n                  )}\n                  {view === \"Commit\" && (\n                    <MonacoDiffEditor\n                      original={data.data.proposed}\n                      modified={data.data.current}\n                      language=\"fancy_toml\"\n                      readOnly\n                    />\n                  )}\n                </>\n              )}\n              {data.type === \"Delete\" && (\n                <MonacoEditor\n                  value={data.data.current}\n                  language=\"fancy_toml\"\n                  readOnly\n                />\n              )}\n            </CardContent>\n          </Card>\n        );\n      })}\n      {/* Pending User Group Update */}\n      {sync?.info?.user_group_updates?.map((data, i) => {\n        return (\n          <Card key={i}>\n            <CardHeader\n              className={cn(\n                \"font-mono pb-2\",\n                text_color_class_by_intention(\n                  diff_type_intention(data.type, view === \"Commit\")\n                )\n              )}\n            >\n              {view === \"Commit\" ? reverse_pending_type(data.type) : data.type}{\" \"}\n              User Group\n            </CardHeader>\n            <CardContent>\n              {data.type === \"Create\" && (\n                <MonacoEditor\n                  value={data.data.proposed}\n                  language=\"fancy_toml\"\n                  readOnly\n                />\n              )}\n              {data.type === \"Update\" && (\n                <>\n                  {view === \"Execute\" && (\n                    <MonacoDiffEditor\n                      original={data.data.current}\n                      modified={data.data.proposed}\n                      language=\"fancy_toml\"\n                      readOnly\n                    />\n                  )}\n                  {view === \"Commit\" && (\n                    <MonacoDiffEditor\n                      original={data.data.proposed}\n                      modified={data.data.current}\n                      language=\"fancy_toml\"\n                      readOnly\n                    />\n                  )}\n                </>\n              )}\n              {data.type === \"Delete\" && (\n                <MonacoEditor\n                  value={data.data.current}\n                  language=\"fancy_toml\"\n                  readOnly\n                />\n              )}\n            </CardContent>\n          </Card>\n        );\n      })}\n    </Section>\n  );\n};\n\nconst reverse_pending_type = (type: ResourceDiff[\"data\"][\"type\"]) => {\n  switch (type) {\n    case \"Create\":\n      return \"Remove\";\n    case \"Update\":\n      return \"Update\";\n    case \"Delete\":\n      return \"Add\";\n  }\n};\n"
  },
  {
    "path": "frontend/src/components/resources/resource-sync/table.tsx",
    "content": "import { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { ResourceLink, StandardSource } from \"../common\";\nimport { TableTags } from \"@components/tags\";\nimport { Types } from \"komodo_client\";\nimport { ResourceSyncComponents } from \".\";\nimport { useSelectedResources } from \"@lib/hooks\";\n\nexport const ResourceSyncTable = ({\n  syncs,\n}: {\n  syncs: Types.ResourceSyncListItem[];\n}) => {\n  const [_, setSelectedResources] = useSelectedResources(\"ResourceSync\");\n  return (\n    <DataTable\n      tableKey=\"syncs\"\n      data={syncs}\n      selectOptions={{\n        selectKey: ({ name }) => name,\n        onSelect: setSelectedResources,\n      }}\n      columns={[\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Name\" />\n          ),\n          accessorKey: \"name\",\n          cell: ({ row }) => (\n            <ResourceLink type=\"ResourceSync\" id={row.original.id} />\n          ),\n          size: 200,\n        },\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Repo\" />\n          ),\n          accessorKey: \"info.repo\",\n          cell: ({ row }) => <StandardSource info={row.original.info} />,\n          size: 200,\n        },\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Branch\" />\n          ),\n          accessorKey: \"info.branch\",\n          size: 200,\n        },\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"State\" />\n          ),\n          accessorKey: \"info.state\",\n          cell: ({ row }) => (\n            <ResourceSyncComponents.State id={row.original.id} />\n          ),\n          size: 120,\n        },\n        {\n          header: \"Tags\",\n          cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/actions.tsx",
    "content": "import { ActionWithDialog, ConfirmButton } from \"@components/util\";\nimport { useExecute, usePermissions, useRead } from \"@lib/hooks\";\nimport { Scissors } from \"lucide-react\";\nimport { useServer } from \".\";\n\nexport const Prune = ({\n  server_id,\n  type,\n}: {\n  server_id: string;\n  type: \"Containers\" | \"Networks\" | \"Images\" | \"Volumes\" | \"Buildx\" | \"System\";\n}) => {\n  const server = useServer(server_id);\n  const { mutate, isPending } = useExecute(`Prune${type}`);\n  const action_state = useRead(\n    \"GetServerActionState\",\n    { server: server_id },\n    { refetchInterval: 5000 }\n  ).data;\n  const { canExecute } = usePermissions({ type: \"Server\", id: server_id });\n\n  if (!server) return;\n\n  const pruningKey =\n    type === \"Containers\"\n      ? \"pruning_containers\"\n      : type === \"Images\"\n        ? \"pruning_images\"\n        : type === \"Networks\"\n          ? \"pruning_networks\"\n          : type === \"Volumes\"\n            ? \"pruning_volumes\"\n            : type === \"Buildx\"\n              ? \"pruning_buildx\"\n              : type === \"System\"\n                ? \"pruning_system\"\n                : \"\";\n\n  const pending = isPending || action_state?.[pruningKey];\n\n  if (type === \"Images\" || type === \"Networks\" || type === \"Buildx\") {\n    return (\n      <ConfirmButton\n        title={`Prune ${type}`}\n        icon={<Scissors className=\"w-4 h-4\" />}\n        onClick={() => mutate({ server: server_id })}\n        loading={pending}\n        disabled={!canExecute || pending}\n      />\n    );\n  } else {\n    return (\n      <ActionWithDialog\n        name={server?.name}\n        title={`Prune ${type}`}\n        icon={<Scissors className=\"w-4 h-4\" />}\n        onClick={() => mutate({ server: server_id })}\n        loading={pending}\n        disabled={!canExecute || pending}\n      />\n    );\n  }\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/config.tsx",
    "content": "import { Config } from \"@components/config\";\nimport { MaintenanceWindows } from \"@components/config/maintenance\";\nimport { ConfigList } from \"@components/config/util\";\nimport {\n  useInvalidate,\n  useLocalStorage,\n  usePermissions,\n  useRead,\n  useWrite,\n} from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { ReactNode } from \"react\";\n\nexport const ServerConfig = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const { canWrite } = usePermissions({ type: \"Server\", id });\n  const invalidate = useInvalidate();\n  const config = useRead(\"GetServer\", { server: id }).data?.config;\n  const global_disabled =\n    useRead(\"GetCoreInfo\", {}).data?.ui_write_disabled ?? false;\n  const [update, set] = useLocalStorage<Partial<Types.ServerConfig>>(\n    `server-${id}-update-v1`,\n    {}\n  );\n  const { mutateAsync } = useWrite(\"UpdateServer\", {\n    onSuccess: () => {\n      // In case of disabling to resolve unreachable alert\n      invalidate([\"ListAlerts\"]);\n    },\n  });\n  if (!config) return null;\n\n  const disabled = global_disabled || !canWrite;\n\n  return (\n    <Config\n      titleOther={titleOther}\n      disabled={disabled}\n      original={config}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: update });\n      }}\n      components={{\n        \"\": [\n          {\n            label: \"Enabled\",\n            labelHidden: true,\n            components: {\n              enabled: {\n                description:\n                  \"Whether to attempt to connect to this host / send alerts if offline. Disabling will also convert all attached resource's state to 'Unknown'.\",\n              },\n            },\n          },\n          {\n            label: \"Address\",\n            labelHidden: true,\n            components: {\n              address: {\n                description:\n                  \"The http/s address of periphery in your network, eg. https://12.34.56.78:8120\",\n                placeholder: \"https://12.34.56.78:8120\",\n              },\n              external_address: {\n                description:\n                  \"Optional. The address of the server used in container links, if different than the Address.\",\n                placeholder: \"https://my.server.int\",\n              },\n              region: {\n                placeholder: \"Region. Optional.\",\n                description:\n                  \"Attach a region to the server for visual grouping.\",\n              },\n            },\n          },\n          {\n            label: \"Timeout\",\n            labelHidden: true,\n            components: {\n              timeout_seconds: {\n                description:\n                  \"The timeout used with the server health check, in seconds.\",\n              },\n            },\n          },\n          {\n            label: \"Disks\",\n            labelHidden: true,\n            components: {\n              ignore_mounts: (values, set) => (\n                <ConfigList\n                  description=\"If undesired disk mount points are coming through in server stats, filter them out here.\"\n                  label=\"Ignore Disks\"\n                  field=\"ignore_mounts\"\n                  values={values ?? []}\n                  set={set}\n                  disabled={disabled}\n                  placeholder=\"/path/to/disk\"\n                />\n              ),\n            },\n          },\n          {\n            label: \"Monitoring\",\n            labelHidden: true,\n            components: {\n              stats_monitoring: {\n                label: \"System Stats Monitoring\",\n                // boldLabel: true,\n                description:\n                  \"Whether to store historical CPU, RAM, and disk usage.\",\n              },\n            },\n          },\n          {\n            label: \"Pruning\",\n            labelHidden: true,\n            components: {\n              auto_prune: {\n                label: \"Auto Prune Images\",\n                // boldLabel: true,\n                description:\n                  \"Whether to prune unused images every day at UTC 00:00\",\n              },\n            },\n          },\n        ],\n        alerts: [\n          {\n            label: \"Unreachable\",\n            labelHidden: true,\n            components: {\n              send_unreachable_alerts: {\n                // boldLabel: true,\n                description:\n                  \"Send an alert if the Periphery agent cannot be reached.\",\n              },\n            },\n          },\n          {\n            label: \"Version\",\n            labelHidden: true,\n            components: {\n              send_version_mismatch_alerts: {\n                label: \"Send Version Mismatch Alerts\",\n                description:\n                  \"Send an alert if the Periphery version differs from the Core version.\",\n              },\n            },\n          },\n          {\n            label: \"CPU\",\n            labelHidden: true,\n            components: {\n              send_cpu_alerts: {\n                label: \"Send CPU Alerts\",\n                // boldLabel: true,\n                description:\n                  \"Send an alert if the CPU usage is above the configured thresholds.\",\n              },\n              cpu_warning: {\n                description:\n                  \"Send a 'Warning' alert if the CPU usage in % is above these thresholds\",\n              },\n              cpu_critical: {\n                description:\n                  \"Send a 'Critical' alert if the CPU usage in % is above these thresholds\",\n              },\n            },\n          },\n          {\n            label: \"Memory\",\n            labelHidden: true,\n            components: {\n              send_mem_alerts: {\n                label: \"Send Memory Alerts\",\n                // boldLabel: true,\n                description:\n                  \"Send an alert if the memory usage is above the configured thresholds.\",\n              },\n              mem_warning: {\n                label: \"Memory Warning\",\n                description:\n                  \"Send a 'Warning' alert if the memory usage in % is above these thresholds\",\n              },\n              mem_critical: {\n                label: \"Memory Critical\",\n                description:\n                  \"Send a 'Critical' alert if the memory usage in % is above these thresholds\",\n              },\n            },\n          },\n          {\n            label: \"Disk\",\n            labelHidden: true,\n            components: {\n              send_disk_alerts: {\n                // boldLabel: true,\n                description:\n                  \"Send an alert if the Disk Usage (for any mounted disk) is above the configured thresholds.\",\n              },\n              disk_warning: {\n                description:\n                  \"Send a 'Warning' alert if the disk usage in % is above these thresholds\",\n              },\n              disk_critical: {\n                description:\n                  \"Send a 'Critical' alert if the disk usage in % is above these thresholds\",\n              },\n            },\n          },\n          {\n            label: \"Maintenance\",\n            boldLabel: false,\n            description: (\n              <>\n                Configure maintenance windows to temporarily disable alerts\n                during scheduled maintenance periods. When a maintenance window\n                is active, alerts from this server will be suppressed.\n              </>\n            ),\n            components: {\n              maintenance_windows: (values, set) => {\n                return (\n                  <MaintenanceWindows\n                    windows={values ?? []}\n                    onUpdate={(maintenance_windows) =>\n                      set({ maintenance_windows })\n                    }\n                    disabled={disabled}\n                  />\n                );\n              },\n            },\n          },\n        ],\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/hooks.ts",
    "content": "import { atomWithStorage } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { useAtom } from \"jotai\";\n\nconst statsGranularityAtom = atomWithStorage<Types.Timelength>(\n  \"stats-granularity-v0\",\n  Types.Timelength.FiveMinutes\n);\n\nexport const useStatsGranularity = () => useAtom(statsGranularityAtom);\n"
  },
  {
    "path": "frontend/src/components/resources/server/index.tsx",
    "content": "import { useExecute, useLocalStorage, useRead, useUser } from \"@lib/hooks\";\nimport { cn } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { RequiredResourceComponents } from \"@types\";\nimport {\n  Server,\n  Cpu,\n  MemoryStick,\n  Database,\n  Play,\n  RefreshCcw,\n  Pause,\n  Square,\n  AlertCircle,\n  CheckCircle2,\n} from \"lucide-react\";\nimport { Section } from \"@components/layouts\";\nimport { Prune } from \"./actions\";\nimport {\n  server_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\nimport { ServerConfig } from \"./config\";\nimport { DeploymentTable } from \"../deployment/table\";\nimport { ServerTable } from \"./table\";\nimport { DeleteResource, NewResource, ResourcePageHeader } from \"../common\";\nimport { ActionWithDialog, ConfirmButton, StatusBadge } from \"@components/util\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@ui/tabs\";\nimport { Card, CardHeader, CardTitle } from \"@ui/card\";\nimport { RepoTable } from \"../repo/table\";\nimport { DashboardPieChart } from \"@pages/home/dashboard\";\nimport { StackTable } from \"../stack/table\";\nimport { ResourceComponents } from \"..\";\nimport { ServerInfo } from \"./info\";\nimport { ServerStats } from \"./stats\";\nimport { ServerStatsMini } from \"./stats-mini\";\nimport { GroupActions } from \"@components/group-actions\";\nimport { ServerTerminals } from \"@components/terminal/server\";\nimport { usePermissions } from \"@lib/hooks\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@ui/tooltip\";\n\nexport const useServer = (id?: string) =>\n  useRead(\"ListServers\", {}, { refetchInterval: 10_000 }).data?.find(\n    (d) => d.id === id\n  );\n\n// Helper function to check if server is available for API calls\nexport const useIsServerAvailable = (serverId?: string) => {\n  const server = useServer(serverId);\n  return server?.info.state === Types.ServerState.Ok;\n};\n\nexport const useFullServer = (id: string) =>\n  useRead(\"GetServer\", { server: id }, { refetchInterval: 10_000 }).data;\n\n// Helper function to check for version mismatch\nexport const useVersionMismatch = (serverId?: string) => {\n  const core_version = useRead(\"GetVersion\", {}).data?.version;\n  const server_version = useServer(serverId)?.info.version;\n\n  const unknown = !server_version || server_version === \"Unknown\";\n  const mismatch =\n    !!server_version && !!core_version && server_version !== core_version;\n\n  return { unknown, mismatch, hasVersionMismatch: mismatch && !unknown };\n};\n\nconst Icon = ({ id, size }: { id?: string; size: number }) => {\n  const state = useServer(id)?.info.state;\n  const { hasVersionMismatch } = useVersionMismatch(id);\n\n  return (\n    <Server\n      className={cn(\n        `w-${size} h-${size}`,\n        state &&\n          stroke_color_class_by_intention(\n            server_state_intention(state, hasVersionMismatch)\n          )\n      )}\n    />\n  );\n};\n\nconst ConfigTabs = ({ id }: { id: string }) => {\n  const [view, setView] = useLocalStorage<\n    \"Config\" | \"Stats\" | \"Docker\" | \"Resources\" | \"Terminals\"\n  >(`server-${id}-tab`, \"Config\");\n\n  const is_admin = useUser().data?.admin ?? false;\n  const { canWrite } = usePermissions({ type: \"Server\", id });\n  const server_info = useServer(id)?.info;\n  const terminals_disabled = server_info?.terminals_disabled ?? true;\n  const container_exec_disabled = server_info?.container_exec_disabled ?? true;\n  const disable_non_admin_create =\n    useRead(\"GetCoreInfo\", {}).data?.disable_non_admin_create ?? true;\n\n  const deployments =\n    useRead(\"ListDeployments\", {}).data?.filter(\n      (deployment) => deployment.info.server_id === id\n    ) ?? [];\n  const noDeployments = deployments.length === 0;\n  const repos =\n    useRead(\"ListRepos\", {}).data?.filter(\n      (repo) => repo.info.server_id === id\n    ) ?? [];\n  const noRepos = repos.length === 0;\n  const stacks =\n    useRead(\"ListStacks\", {}).data?.filter(\n      (stack) => stack.info.server_id === id\n    ) ?? [];\n  const noStacks = stacks.length === 0;\n\n  const noResources = noDeployments && noRepos && noStacks;\n\n  const currentView = view === \"Resources\" && noResources ? \"Config\" : view;\n\n  const tabsList = (\n    <TabsList className=\"justify-start w-fit\">\n      <TabsTrigger value=\"Config\" className=\"w-[110px]\">\n        Config\n      </TabsTrigger>\n\n      <TabsTrigger value=\"Stats\" className=\"w-[110px]\">\n        Stats\n      </TabsTrigger>\n\n      <TabsTrigger value=\"Docker\" className=\"w-[110px]\">\n        Docker\n      </TabsTrigger>\n\n      <TabsTrigger\n        value=\"Resources\"\n        className=\"w-[110px]\"\n        disabled={noResources}\n      >\n        Resources\n      </TabsTrigger>\n\n      {(!terminals_disabled || !container_exec_disabled) && canWrite && (\n        <TabsTrigger value=\"Terminals\" className=\"w-[110px]\">\n          Terminals\n        </TabsTrigger>\n      )}\n    </TabsList>\n  );\n  return (\n    <Tabs value={currentView} onValueChange={setView as any}>\n      <TabsContent value=\"Config\">\n        <ServerConfig id={id} titleOther={tabsList} />\n      </TabsContent>\n\n      <TabsContent value=\"Stats\">\n        <ServerStats id={id} titleOther={tabsList} />\n      </TabsContent>\n\n      <TabsContent value=\"Docker\">\n        <ServerInfo id={id} titleOther={tabsList} />\n      </TabsContent>\n\n      <TabsContent value=\"Resources\">\n        <Section titleOther={tabsList}>\n          <Section\n            title=\"Deployments\"\n            actions={\n              (is_admin || !disable_non_admin_create) && (\n                <ResourceComponents.Deployment.New server_id={id} />\n              )\n            }\n          >\n            <DeploymentTable deployments={deployments} />\n          </Section>\n          <Section\n            title=\"Stacks\"\n            actions={\n              (is_admin || !disable_non_admin_create) && (\n                <ResourceComponents.Stack.New server_id={id} />\n              )\n            }\n          >\n            <StackTable stacks={stacks} />\n          </Section>\n          <Section\n            title=\"Repos\"\n            actions={\n              (is_admin || !disable_non_admin_create) && (\n                <ResourceComponents.Repo.New server_id={id} />\n              )\n            }\n          >\n            <RepoTable repos={repos} />\n          </Section>\n        </Section>\n      </TabsContent>\n\n      <TabsContent value=\"Terminals\">\n        {(!terminals_disabled || !container_exec_disabled) && canWrite && (\n          <ServerTerminals id={id} titleOther={tabsList} />\n        )}\n        {terminals_disabled && container_exec_disabled && canWrite && (\n          <Section titleOther={tabsList}>\n            <Card>\n              <CardHeader>\n                <CardTitle>Terminals are disabled on this Server.</CardTitle>\n              </CardHeader>\n            </Card>\n          </Section>\n        )}\n        {!canWrite && (\n          <Section titleOther={tabsList}>\n            <Card>\n              <CardHeader>\n                <CardTitle>\n                  User does not have permission to use Terminals.\n                </CardTitle>\n              </CardHeader>\n            </Card>\n          </Section>\n        )}\n      </TabsContent>\n    </Tabs>\n  );\n};\n\nexport const ServerVersion = ({ id }: { id: string }) => {\n  const core_version = useRead(\"GetVersion\", {}).data?.version;\n  const version = useServer(id)?.info.version;\n  const server_state = useServer(id)?.info.state;\n\n  const unknown = !version || version === \"Unknown\";\n  const mismatch = !!version && !!core_version && version !== core_version;\n\n  // Don't show version for disabled servers\n  if (server_state === Types.ServerState.Disabled) {\n    return (\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <div className=\"flex items-center gap-2 cursor-pointer\">\n            <AlertCircle\n              className={cn(\n                \"w-4 h-4\",\n                stroke_color_class_by_intention(\"Unknown\")\n              )}\n            />\n            Unknown\n          </div>\n        </TooltipTrigger>\n        <TooltipContent>\n          <div>\n            Server is <span className=\"font-bold\">disabled</span> - version\n            unknown.\n          </div>\n        </TooltipContent>\n      </Tooltip>\n    );\n  }\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <div className=\"flex items-center gap-2 cursor-pointer\">\n          {unknown ? (\n            <AlertCircle\n              className={cn(\n                \"w-4 h-4\",\n                stroke_color_class_by_intention(\"Unknown\")\n              )}\n            />\n          ) : mismatch ? (\n            <AlertCircle\n              className={cn(\n                \"w-4 h-4\",\n                stroke_color_class_by_intention(\"Critical\")\n              )}\n            />\n          ) : (\n            <CheckCircle2\n              className={cn(\"w-4 h-4\", stroke_color_class_by_intention(\"Good\"))}\n            />\n          )}\n          {version ?? \"Unknown\"}\n        </div>\n      </TooltipTrigger>\n      <TooltipContent>\n        {unknown ? (\n          <div>\n            Periphery version is <span className=\"font-bold\">unknown</span>.\n          </div>\n        ) : mismatch ? (\n          <div>\n            Periphery version <span className=\"font-bold\">mismatch</span>.\n            Expected <span className=\"font-bold\">{core_version}</span>.\n          </div>\n        ) : (\n          <div>\n            Periphery and Core version <span className=\"font-bold\">match</span>.\n          </div>\n        )}\n      </TooltipContent>\n    </Tooltip>\n  );\n};\n\nexport { ServerStatsMini };\n\nexport const ServerComponents: RequiredResourceComponents = {\n  list_item: (id) => useServer(id),\n  resource_links: (resource) => (resource.config as Types.ServerConfig).links,\n\n  Description: () => (\n    <>Connect servers for alerting, building, and deploying.</>\n  ),\n\n  Dashboard: () => {\n    const summary = useRead(\n      \"GetServersSummary\",\n      {},\n      { refetchInterval: 15_000 }\n    ).data;\n    return (\n      <DashboardPieChart\n        data={[\n          { title: \"Healthy\", intention: \"Good\", value: summary?.healthy ?? 0 },\n          {\n            title: \"Warning\",\n            intention: \"Warning\",\n            value: summary?.warning ?? 0,\n          },\n          {\n            title: \"Unhealthy\",\n            intention: \"Critical\",\n            value: summary?.unhealthy ?? 0,\n          },\n          {\n            title: \"Disabled\",\n            intention: \"Neutral\",\n            value: summary?.disabled ?? 0,\n          },\n        ]}\n      />\n    );\n  },\n\n  New: () => {\n    const user = useUser().data;\n    if (!user) return null;\n    if (!user.admin && !user.create_server_permissions) return null;\n    return <NewResource type=\"Server\" />;\n  },\n\n  GroupActions: () => (\n    <GroupActions\n      type=\"Server\"\n      actions={[\n        \"PruneContainers\",\n        \"PruneNetworks\",\n        \"PruneVolumes\",\n        \"PruneImages\",\n        \"PruneSystem\",\n        \"RestartAllContainers\",\n        \"StopAllContainers\",\n      ]}\n    />\n  ),\n\n  Table: ({ resources }) => (\n    <ServerTable servers={resources as Types.ServerListItem[]} />\n  ),\n\n  Icon: ({ id }) => <Icon id={id} size={4} />,\n  BigIcon: ({ id }) => <Icon id={id} size={8} />,\n\n  State: ({ id }) => {\n    const state = useServer(id)?.info.state;\n    const { hasVersionMismatch } = useVersionMismatch(id);\n\n    // Show full version mismatch text\n    const displayState =\n      state === Types.ServerState.Ok && hasVersionMismatch\n        ? \"Version Mismatch\"\n        : state === Types.ServerState.NotOk\n          ? \"Not Ok\"\n          : state;\n\n    return (\n      <StatusBadge\n        text={displayState}\n        intent={server_state_intention(state, hasVersionMismatch)}\n      />\n    );\n  },\n\n  Status: {},\n\n  Info: {\n    Version: ServerVersion,\n    Cpu: ({ id }) => {\n      const isServerAvailable = useIsServerAvailable(id);\n      const core_count =\n        useRead(\n          \"GetSystemInformation\",\n          { server: id },\n          {\n            enabled: isServerAvailable,\n            refetchInterval: 5000,\n          }\n        ).data?.core_count ?? 0;\n      return (\n        <div className=\"flex gap-2 items-center\">\n          <Cpu className=\"w-4 h-4\" />\n          {core_count || \"N/A\"} Core{core_count > 1 ? \"s\" : \"\"}\n        </div>\n      );\n    },\n    LoadAvg: ({ id }) => {\n      const isServerAvailable = useIsServerAvailable(id);\n      const stats = useRead(\n        \"GetSystemStats\",\n        { server: id },\n        {\n          enabled: isServerAvailable,\n          refetchInterval: 5000,\n        }\n      ).data;\n\n      if (!stats?.load_average) return null;\n      const one = stats.load_average?.one;\n\n      return (\n        <div className=\"flex gap-2 items-center\">\n          <Cpu className=\"w-4 h-4\" />\n          {one.toFixed(2)}\n        </div>\n      );\n    },\n    Mem: ({ id }) => {\n      const isServerAvailable = useIsServerAvailable(id);\n      const stats = useRead(\n        \"GetSystemStats\",\n        { server: id },\n        {\n          enabled: isServerAvailable,\n          refetchInterval: 5000,\n        }\n      ).data;\n      return (\n        <div className=\"flex gap-2 items-center\">\n          <MemoryStick className=\"w-4 h-4\" />\n          {stats?.mem_total_gb.toFixed(2) ?? \"N/A\"} GB\n        </div>\n      );\n    },\n    Disk: ({ id }) => {\n      const isServerAvailable = useIsServerAvailable(id);\n      const stats = useRead(\n        \"GetSystemStats\",\n        { server: id },\n        {\n          enabled: isServerAvailable,\n          refetchInterval: 5000,\n        }\n      ).data;\n      const disk_total_gb = stats?.disks.reduce(\n        (acc, curr) => acc + curr.total_gb,\n        0\n      );\n      return (\n        <div className=\"flex gap-2 items-center\">\n          <Database className=\"w-4 h-4\" />\n          {disk_total_gb?.toFixed(2) ?? \"N/A\"} GB\n        </div>\n      );\n    },\n  },\n\n  Actions: {\n    StartAll: ({ id }) => {\n      const server = useServer(id);\n      const { mutate, isPending } = useExecute(\"StartAllContainers\");\n      const starting = useRead(\n        \"GetServerActionState\",\n        { server: id },\n        { refetchInterval: 5000 }\n      ).data?.starting_containers;\n      const dontShow =\n        useRead(\"ListDockerContainers\", {\n          server: id,\n        }).data?.every(\n          (container) =>\n            container.state === Types.ContainerStateStatusEnum.Running\n        ) ?? true;\n      if (dontShow) {\n        return null;\n      }\n      const pending = isPending || starting;\n      return (\n        server && (\n          <ConfirmButton\n            title=\"Start Containers\"\n            icon={<Play className=\"w-4 h-4\" />}\n            onClick={() => mutate({ server: id })}\n            loading={pending}\n            disabled={pending}\n          />\n        )\n      );\n    },\n    RestartAll: ({ id }) => {\n      const server = useServer(id);\n      const { mutate, isPending } = useExecute(\"RestartAllContainers\");\n      const restarting = useRead(\n        \"GetServerActionState\",\n        { server: id },\n        { refetchInterval: 5000 }\n      ).data?.restarting_containers;\n      const pending = isPending || restarting;\n      return (\n        server && (\n          <ActionWithDialog\n            name={server?.name}\n            title=\"Restart Containers\"\n            icon={<RefreshCcw className=\"w-4 h-4\" />}\n            onClick={() => mutate({ server: id })}\n            disabled={pending}\n            loading={pending}\n          />\n        )\n      );\n    },\n    PauseAll: ({ id }) => {\n      const server = useServer(id);\n      const { mutate, isPending } = useExecute(\"PauseAllContainers\");\n      const pausing = useRead(\n        \"GetServerActionState\",\n        { server: id },\n        { refetchInterval: 5000 }\n      ).data?.pausing_containers;\n      const dontShow =\n        useRead(\"ListDockerContainers\", {\n          server: id,\n        }).data?.every(\n          (container) =>\n            container.state !== Types.ContainerStateStatusEnum.Running\n        ) ?? true;\n      if (dontShow) {\n        return null;\n      }\n      const pending = isPending || pausing;\n      return (\n        server && (\n          <ActionWithDialog\n            name={server?.name}\n            title=\"Pause Containers\"\n            icon={<Pause className=\"w-4 h-4\" />}\n            onClick={() => mutate({ server: id })}\n            disabled={pending}\n            loading={pending}\n          />\n        )\n      );\n    },\n    UnpauseAll: ({ id }) => {\n      const server = useServer(id);\n      const { mutate, isPending } = useExecute(\"UnpauseAllContainers\");\n      const unpausing = useRead(\n        \"GetServerActionState\",\n        { server: id },\n        { refetchInterval: 5000 }\n      ).data?.unpausing_containers;\n      const dontShow =\n        useRead(\"ListDockerContainers\", {\n          server: id,\n        }).data?.every(\n          (container) =>\n            container.state !== Types.ContainerStateStatusEnum.Paused\n        ) ?? true;\n      if (dontShow) {\n        return null;\n      }\n      const pending = isPending || unpausing;\n      return (\n        server && (\n          <ConfirmButton\n            title=\"Unpause Containers\"\n            icon={<Play className=\"w-4 h-4\" />}\n            onClick={() => mutate({ server: id })}\n            loading={pending}\n            disabled={pending}\n          />\n        )\n      );\n    },\n    StopAll: ({ id }) => {\n      const server = useServer(id);\n      const { mutate, isPending } = useExecute(\"StopAllContainers\");\n      const stopping = useRead(\n        \"GetServerActionState\",\n        { server: id },\n        { refetchInterval: 5000 }\n      ).data?.stopping_containers;\n      const pending = isPending || stopping;\n      return (\n        server && (\n          <ActionWithDialog\n            name={server.name}\n            title=\"Stop Containers\"\n            icon={<Square className=\"w-4 h-4\" />}\n            onClick={() => mutate({ server: id })}\n            disabled={pending}\n            loading={pending}\n          />\n        )\n      );\n    },\n    PruneBuildx: ({ id }) => <Prune server_id={id} type=\"Buildx\" />,\n    PruneSystem: ({ id }) => <Prune server_id={id} type=\"System\" />,\n  },\n\n  Page: {},\n\n  Config: ConfigTabs,\n\n  DangerZone: ({ id }) => <DeleteResource type=\"Server\" id={id} />,\n\n  ResourcePageHeader: ({ id }) => {\n    const server = useServer(id);\n    const { hasVersionMismatch } = useVersionMismatch(id);\n\n    // Determine display state for header (longer text is okay in header)\n    const displayState =\n      server?.info.state === Types.ServerState.Ok && hasVersionMismatch\n        ? \"Version Mismatch\"\n        : server?.info.state === Types.ServerState.NotOk\n          ? \"Not Ok\"\n          : server?.info.state;\n\n    return (\n      <ResourcePageHeader\n        intent={server_state_intention(server?.info.state, hasVersionMismatch)}\n        icon={<Icon id={id} size={8} />}\n        type=\"Server\"\n        id={id}\n        resource={server}\n        state={displayState}\n        status={server?.info.region}\n      />\n    );\n  },\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/info/containers.tsx",
    "content": "import { DockerContainersSection } from \"@components/util\";\nimport { useRead } from \"@lib/hooks\";\nimport { Dispatch, ReactNode, SetStateAction } from \"react\";\n\nexport const Containers = ({\n  id,\n  titleOther,\n  _search,\n}: {\n  id: string;\n  titleOther: ReactNode;\n  _search: [string, Dispatch<SetStateAction<string>>];\n}) => {\n  const containers =\n    useRead(\"ListDockerContainers\", { server: id }, { refetchInterval: 10_000 })\n      .data ?? [];\n  return (\n    <DockerContainersSection\n      server_id={id}\n      containers={containers}\n      titleOther={titleOther}\n      _search={_search}\n      pruneButton\n      forceTall\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/info/images.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { DockerResourceLink } from \"@components/util\";\nimport { format_size_bytes } from \"@lib/formatting\";\nimport { useRead } from \"@lib/hooks\";\nimport { Badge } from \"@ui/badge\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { Dispatch, ReactNode, SetStateAction } from \"react\";\nimport { Prune } from \"../actions\";\nimport { Search } from \"lucide-react\";\nimport { Input } from \"@ui/input\";\nimport { filterBySplit } from \"@lib/utils\";\n\nexport const Images = ({\n  id,\n  titleOther,\n  _search,\n}: {\n  id: string;\n  titleOther: ReactNode;\n  _search: [string, Dispatch<SetStateAction<string>>];\n}) => {\n  const [search, setSearch] = _search;\n  const images =\n    useRead(\"ListDockerImages\", { server: id }, { refetchInterval: 10_000 })\n      .data ?? [];\n\n  const allInUse = images.every((image) => image.in_use);\n\n  const filtered = filterBySplit(images, search, (image) => image.name);\n\n  return (\n    <Section\n      titleOther={titleOther}\n      actions={\n        <div className=\"flex items-center gap-4\">\n          {!allInUse && <Prune server_id={id} type=\"Images\" />}\n          <div className=\"relative\">\n            <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n            <Input\n              value={search}\n              onChange={(e) => setSearch(e.target.value)}\n              placeholder=\"search...\"\n              className=\"pl-8 w-[200px] lg:w-[300px]\"\n            />\n          </div>\n        </div>\n      }\n    >\n      <DataTable\n        containerClassName=\"min-h-[60vh]\"\n        tableKey=\"server-images\"\n        data={filtered}\n        columns={[\n          {\n            accessorKey: \"name\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Name\" />\n            ),\n            cell: ({ row }) => (\n              <DockerResourceLink\n                type=\"image\"\n                server_id={id}\n                name={row.original.name}\n                id={row.original.id}\n                extra={\n                  !row.original.in_use && (\n                    <Badge variant=\"destructive\">Unused</Badge>\n                  )\n                }\n              />\n            ),\n            size: 200,\n          },\n          {\n            accessorKey: \"id\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Id\" />\n            ),\n          },\n          {\n            accessorKey: \"size\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Size\" />\n            ),\n            cell: ({ row }) =>\n              row.original.size\n                ? format_size_bytes(row.original.size)\n                : \"Unknown\",\n          },\n        ]}\n      />\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/info/index.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { ReactNode, useState } from \"react\";\nimport { Networks } from \"./networks\";\nimport { useServer } from \"..\";\nimport { Types } from \"komodo_client\";\nimport { useLocalStorage } from \"@lib/hooks\";\nimport { Images } from \"./images\";\nimport { Containers } from \"./containers\";\nimport { Volumes } from \"./volumes\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@ui/tabs\";\n\nexport const ServerInfo = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const _search = useState(\"\");\n  const state = useServer(id)?.info.state ?? Types.ServerState.NotOk;\n  const [show2, setShow2] = useLocalStorage<\n    \"Containers\" | \"Networks\" | \"Volumes\" | \"Images\"\n  >(\"server-info-show-config-v2\", \"Containers\");\n\n  if ([Types.ServerState.NotOk, Types.ServerState.Disabled].includes(state)) {\n    return (\n      <Section titleOther={titleOther}>\n        <h2 className=\"text-muted-foreground\">\n          Server unreachable, info is not available\n        </h2>\n      </Section>\n    );\n  }\n\n  const tabsList = (\n    <TabsList className=\"justify-start w-fit\">\n      <TabsTrigger value=\"Containers\" className=\"w-[110px]\">\n        Containers\n      </TabsTrigger>\n      <TabsTrigger value=\"Networks\" className=\"w-[110px]\">\n        Networks\n      </TabsTrigger>\n      <TabsTrigger value=\"Volumes\" className=\"w-[110px]\">\n        Volumes\n      </TabsTrigger>\n      <TabsTrigger value=\"Images\" className=\"w-[110px]\">\n        Images\n      </TabsTrigger>\n    </TabsList>\n  );\n\n  return (\n    <Section titleOther={titleOther}>\n      <Tabs value={show2} onValueChange={setShow2 as any}>\n        <TabsContent value=\"Containers\">\n          <Containers id={id} titleOther={tabsList} _search={_search} />\n        </TabsContent>\n        <TabsContent value=\"Networks\">\n          <Networks id={id} titleOther={tabsList} _search={_search} />\n        </TabsContent>\n        <TabsContent value=\"Volumes\">\n          <Volumes id={id} titleOther={tabsList} _search={_search} />\n        </TabsContent>\n        <TabsContent value=\"Images\">\n          <Images id={id} titleOther={tabsList} _search={_search} />\n        </TabsContent>\n      </Tabs>\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/info/networks.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { DockerResourceLink } from \"@components/util\";\nimport { useRead } from \"@lib/hooks\";\nimport { Badge } from \"@ui/badge\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { Dispatch, ReactNode, SetStateAction } from \"react\";\nimport { Prune } from \"../actions\";\nimport { filterBySplit } from \"@lib/utils\";\nimport { Search } from \"lucide-react\";\nimport { Input } from \"@ui/input\";\n\nexport const Networks = ({\n  id,\n  titleOther,\n  _search\n}: {\n  id: string;\n  titleOther: ReactNode;\n  _search: [string, Dispatch<SetStateAction<string>>];\n}) => {\n  const [search, setSearch] = _search;\n  const networks =\n    useRead(\"ListDockerNetworks\", { server: id }, { refetchInterval: 10_000 })\n      .data ?? [];\n\n  const allInUse = networks.every((network) =>\n    // this ignores networks that come in with no name, but they should all come in with name\n    !network.name\n      ? true\n      : [\"none\", \"host\", \"bridge\"].includes(network.name)\n        ? true\n        : network.in_use\n  );\n\n  const filtered = filterBySplit(\n    networks,\n    search,\n    (network) => network.name ?? \"\"\n  );\n\n  return (\n    <Section\n      titleOther={titleOther}\n      actions={\n        <div className=\"flex items-center gap-4\">\n          {!allInUse && <Prune server_id={id} type=\"Networks\" />}\n          <div className=\"relative\">\n            <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n            <Input\n              value={search}\n              onChange={(e) => setSearch(e.target.value)}\n              placeholder=\"search...\"\n              className=\"pl-8 w-[200px] lg:w-[300px]\"\n            />\n          </div>\n        </div>\n      }\n    >\n      <DataTable\n        containerClassName=\"min-h-[60vh]\"\n        tableKey=\"server-networks\"\n        data={filtered}\n        columns={[\n          {\n            accessorKey: \"name\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Name\" />\n            ),\n            cell: ({ row }) => (\n              <div className=\"flex items-center gap-2\">\n                <DockerResourceLink\n                  type=\"network\"\n                  server_id={id}\n                  name={row.original.name}\n                  extra={\n                    [\"none\", \"host\", \"bridge\"].includes(\n                      row.original.name ?? \"\"\n                    ) ? (\n                      <Badge variant=\"outline\">System</Badge>\n                    ) : (\n                      !row.original.in_use && (\n                        <Badge variant=\"destructive\">Unused</Badge>\n                      )\n                    )\n                  }\n                />\n              </div>\n            ),\n            size: 300,\n          },\n          {\n            accessorKey: \"driver\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Driver\" />\n            ),\n          },\n          {\n            accessorKey: \"scope\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Scope\" />\n            ),\n          },\n          {\n            accessorKey: \"attachable\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Attachable\" />\n            ),\n          },\n          {\n            accessorKey: \"ipam_driver\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"IPAM Driver\" />\n            ),\n          },\n        ]}\n      />\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/info/volumes.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { DockerResourceLink } from \"@components/util\";\nimport { useRead } from \"@lib/hooks\";\nimport { Badge } from \"@ui/badge\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { Dispatch, ReactNode, SetStateAction } from \"react\";\nimport { Prune } from \"../actions\";\nimport { Search } from \"lucide-react\";\nimport { Input } from \"@ui/input\";\nimport { filterBySplit } from \"@lib/utils\";\n\nexport const Volumes = ({\n  id,\n  titleOther,\n  _search,\n}: {\n  id: string;\n  titleOther: ReactNode;\n  _search: [string, Dispatch<SetStateAction<string>>];\n}) => {\n  const [search, setSearch] = _search;\n  const volumes =\n    useRead(\"ListDockerVolumes\", { server: id }, { refetchInterval: 10_000 })\n      .data ?? [];\n\n  const allInUse = volumes.every((volume) => volume.in_use);\n\n  const filtered = filterBySplit(volumes, search, (volume) => volume.name);\n\n  return (\n    <Section\n      titleOther={titleOther}\n      actions={\n        <div className=\"flex items-center gap-4\">\n          {!allInUse && <Prune server_id={id} type=\"Volumes\" />}\n          <div className=\"relative\">\n            <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n            <Input\n              value={search}\n              onChange={(e) => setSearch(e.target.value)}\n              placeholder=\"search...\"\n              className=\"pl-8 w-[200px] lg:w-[300px]\"\n            />\n          </div>\n        </div>\n      }\n    >\n      <DataTable\n        containerClassName=\"min-h-[60vh]\"\n        tableKey=\"server-volumes\"\n        data={filtered}\n        columns={[\n          {\n            accessorKey: \"name\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Name\" />\n            ),\n            cell: ({ row }) => (\n              <DockerResourceLink\n                type=\"volume\"\n                server_id={id}\n                name={row.original.name}\n                extra={\n                  !row.original.in_use && (\n                    <Badge variant=\"destructive\">Unused</Badge>\n                  )\n                }\n              />\n            ),\n            size: 200,\n          },\n          {\n            accessorKey: \"driver\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Driver\" />\n            ),\n          },\n          {\n            accessorKey: \"scope\",\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Scope\" />\n            ),\n          },\n        ]}\n      />\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/monitoring-table.tsx",
    "content": "import { ResourceLink } from \"@components/resources/common\";\nimport { ServerComponents } from \"@components/resources/server\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { useRead } from \"@lib/hooks\";\nimport { useMemo } from \"react\";\nimport { useIsServerAvailable } from \".\";\n\nexport const ServerMonitoringTable = ({ search = \"\" }: { search?: string }) => {\n  const servers = useRead(\"ListServers\", {}).data;\n  const searchSplit = useMemo(\n    () => search.toLowerCase().split(\" \").filter((t) => t),\n    [search]\n  );\n  const filtered = useMemo(\n    () =>\n      servers?.filter((s) =>\n        searchSplit.length === 0\n          ? true\n          : searchSplit.every((t) => s.name.toLowerCase().includes(t))\n      ) ?? [],\n    [servers, searchSplit]\n  );\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <DataTable<any, any>\n        tableKey=\"servers-monitoring-v1\"\n        data={filtered}\n        columns={[\n          {\n            accessorKey: \"name\",\n            size: 250,\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"System\" />\n            ),\n            cell: ({ row }) => (\n              <div className=\"flex items-center gap-2\">\n                <ResourceLink type=\"Server\" id={row.original.id} />\n              </div>\n            ),\n          },\n          {\n            header: \"CPU\",\n            size: 200,\n            cell: ({ row }) => <CpuCell id={row.original.id} />,\n          },\n          {\n            header: \"Memory\",\n            size: 200,\n            cell: ({ row }) => <MemCell id={row.original.id} />,\n          },\n          {\n            header: \"Disk\",\n            size: 200,\n            cell: ({ row }) => <DiskCell id={row.original.id} />,\n          },\n          {\n            header: \"Load Avg\",\n            size: 160,\n            cell: ({ row }) => <LoadAvgCell id={row.original.id} />,\n          },\n          {\n            header: \"Net\",\n            size: 100,\n            cell: ({ row }) => <NetCell id={row.original.id} />,\n          },\n          {\n            header: \"Agent\",\n            size: 160,\n            cell: ({ row }) => <ServerComponents.Info.Version id={row.original.id} />,\n          },\n        ]}\n      />\n    </div>\n  );\n};\n\nconst useStats = (id: string) => {\n  const isServerAvailable = useIsServerAvailable(id);\n  return useRead(\"GetSystemStats\", { server: id }, { \n    enabled: isServerAvailable,\n    refetchInterval: 10_000 \n  }).data;\n};\n\nconst useServerThresholds = (id: string) => {\n  const isServerAvailable = useIsServerAvailable(id);\n  const config = useRead(\"GetServer\", { server: id }, {\n    enabled: isServerAvailable\n  }).data?.config as any;\n  return {\n    cpuWarning: config?.cpu_warning ?? 75,\n    cpuCritical: config?.cpu_critical ?? 90,\n    memWarning: config?.mem_warning ?? 75,\n    memCritical: config?.mem_critical ?? 90,\n    diskWarning: config?.disk_warning ?? 75,\n    diskCritical: config?.disk_critical ?? 90,\n  };\n};\n\nconst Bar = ({ valuePerc, intent }: { valuePerc?: number; intent: \"Good\" | \"Warning\" | \"Critical\" }) => {\n  const w = Math.max(0, Math.min(100, valuePerc ?? 0)) / 100;\n  const color = intent === \"Good\" ? \"bg-green-500\" : intent === \"Warning\" ? \"bg-orange-500\" : \"bg-red-500\";\n  return (\n    <span className=\"grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden\">\n      <span className={`absolute inset-0 w-full h-full origin-left ${color}`} style={{ transform: `scaleX(${w})` }} />\n    </span>\n  );\n};\n\nconst CpuCell = ({ id }: { id: string }) => {\n  const stats = useStats(id);\n  const cpu = stats?.cpu_perc ?? 0;\n  const { cpuWarning: warning, cpuCritical: critical } = useServerThresholds(id);\n  const intent: \"Good\" | \"Warning\" | \"Critical\" =\n    cpu < warning ? \"Good\" : cpu < critical ? \"Warning\" : \"Critical\";\n  return (\n    <div className=\"flex gap-2 items-center tabular-nums tracking-tight\">\n      <span className=\"min-w-8\">{cpu.toFixed(2)}%</span>\n      <Bar valuePerc={cpu} intent={intent} />\n    </div>\n  );\n};\n\nconst MemCell = ({ id }: { id: string }) => {\n  const stats = useStats(id);\n  const used = stats?.mem_used_gb ?? 0;\n  const total = stats?.mem_total_gb ?? 0;\n  const perc = total > 0 ? (used / total) * 100 : 0;\n  const { memWarning: warning, memCritical: critical } = useServerThresholds(id);\n  const intent: \"Good\" | \"Warning\" | \"Critical\" =\n    perc < warning ? \"Good\" : perc < critical ? \"Warning\" : \"Critical\";\n  return (\n    <div className=\"flex gap-2 items-center tabular-nums tracking-tight\">\n      <span className=\"min-w-8\">{perc.toFixed(1)}%</span>\n      <Bar valuePerc={perc} intent={intent} />\n    </div>\n  );\n};\n\nconst DiskCell = ({ id }: { id: string }) => {\n  const stats = useStats(id);\n  const used = stats?.disks?.reduce((acc, d) => acc + (d.used_gb || 0), 0) ?? 0;\n  const total = stats?.disks?.reduce((acc, d) => acc + (d.total_gb || 0), 0) ?? 0;\n  const perc = total > 0 ? (used / total) * 100 : 0;\n  const { diskWarning: warning, diskCritical: critical } = useServerThresholds(id);\n  const intent: \"Good\" | \"Warning\" | \"Critical\" =\n    perc < warning ? \"Good\" : perc < critical ? \"Warning\" : \"Critical\";\n  return (\n    <div className=\"flex gap-2 items-center tabular-nums tracking-tight\">\n      <span className=\"min-w-8\">{perc.toFixed(1)}%</span>\n      <Bar valuePerc={perc} intent={intent} />\n    </div>\n  );\n};\n\nconst formatRate = (bytes?: number) => {\n  const b = bytes ?? 0;\n  const kb = 1024;\n  const mb = kb * 1024;\n  const gb = mb * 1024;\n  if (b >= gb) return `${(b / gb).toFixed(2)} GB/s`;\n  if (b >= mb) return `${(b / mb).toFixed(2)} MB/s`;\n  if (b >= kb) return `${(b / kb).toFixed(2)} KB/s`;\n  return `${b.toFixed(0)} B/s`;\n};\n\nconst NetCell = ({ id }: { id: string }) => {\n  const stats = useStats(id);\n  const ingress = stats?.network_ingress_bytes ?? 0;\n  const egress = stats?.network_egress_bytes ?? 0;\n  return (\n    <span className=\"tabular-nums whitespace-nowrap\">\n      {formatRate(ingress + egress)}\n    </span>\n  );\n};\n\nconst LoadAvgCell = ({ id }: { id: string }) => {\n  const stats = useStats(id);\n  const one = stats?.load_average?.one;\n  const five = stats?.load_average?.five;\n  const fifteen = stats?.load_average?.fifteen;\n  if (one === undefined || five === undefined || fifteen === undefined) {\n    return (\n      <div className=\"w-full flex items-center gap-[.35em] tabular-nums text-muted-foreground tracking-tight\">\n        <span>N/A</span>\n      </div>\n    );\n  }\n  return (\n    <div className=\"w-full flex items-center gap-[.35em] tabular-nums tracking-tight\">\n      <span>{one.toFixed(2)}</span>\n      <span>{five.toFixed(2)}</span>\n      <span>{fifteen.toFixed(2)}</span>\n    </div>\n  );\n};\n\n\n"
  },
  {
    "path": "frontend/src/components/resources/server/stat-chart.tsx",
    "content": "import { hex_color_by_intention } from \"@lib/color\";\nimport { useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { useMemo } from \"react\";\nimport { useStatsGranularity } from \"./hooks\";\nimport { Loader2, OctagonAlert } from \"lucide-react\";\nimport { AxisOptions, Chart } from \"react-charts\";\nimport { convertTsMsToLocalUnixTsInMs } from \"@lib/utils\";\nimport { useTheme } from \"@ui/theme\";\nimport { fmt_utc_date } from \"@lib/formatting\";\n\ntype StatType =\n  | \"Cpu\"\n  | \"Memory\"\n  | \"Disk\"\n  | \"Network Ingress\"\n  | \"Network Egress\"\n  | \"Load Average\";\n\ntype StatDatapoint = { date: number; value: number };\n\nexport const StatChart = ({\n  server_id,\n  type,\n  className,\n}: {\n  server_id: string;\n  type: StatType;\n  className?: string;\n}) => {\n  const [granularity] = useStatsGranularity();\n\n  const { data, isPending } = useRead(\"GetHistoricalServerStats\", {\n    server: server_id,\n    granularity,\n  });\n\n  const seriesData = useMemo(() => {\n    if (!data?.stats) return [] as { label: string; data: StatDatapoint[] }[];\n    const records = [...data.stats].reverse();\n    if (type === \"Load Average\") {\n      const one = records.map((s) => ({\n        date: convertTsMsToLocalUnixTsInMs(s.ts),\n        value: s.load_average?.one ?? 0,\n      }));\n      const five = records.map((s) => ({\n        date: convertTsMsToLocalUnixTsInMs(s.ts),\n        value: s.load_average?.five ?? 0,\n      }));\n      const fifteen = records.map((s) => ({\n        date: convertTsMsToLocalUnixTsInMs(s.ts),\n        value: s.load_average?.fifteen ?? 0,\n      }));\n      return [\n        { label: \"1m\", data: one },\n        { label: \"5m\", data: five },\n        { label: \"15m\", data: fifteen },\n      ];\n    }\n    const single = records.map((stat) => ({\n      date: convertTsMsToLocalUnixTsInMs(stat.ts),\n      value: getStat(stat, type),\n    }));\n    return [{ label: type, data: single }];\n  }, [data, type]);\n\n  return (\n    <div className={className}>\n      <h1 className=\"px-2 py-1\">{type}</h1>\n      {isPending ? (\n        <div className=\"w-full max-w-full h-full flex items-center justify-center\">\n          <Loader2 className=\"w-8 h-8 animate-spin\" />\n        </div>\n      ) : (\n        <InnerStatChart\n          type={type}\n          stats={seriesData.flatMap((s) => s.data)}\n          seriesData={seriesData}\n        />\n      )}\n    </div>\n  );\n};\n\nconst BYTES_PER_GB = 1073741824.0;\nconst BYTES_PER_MB = 1048576.0;\nconst BYTES_PER_KB = 1024.0;\n\nexport const InnerStatChart = ({\n  type,\n  stats,\n  seriesData,\n}: {\n  type: StatType;\n  stats: StatDatapoint[] | undefined;\n  seriesData?: { label: string; data: StatDatapoint[] }[];\n}) => {\n  const { currentTheme } = useTheme();\n\n  const min = stats?.[0]?.date ?? 0;\n  const max = stats?.[stats.length - 1]?.date ?? 0;\n  const diff = max - min;\n\n  const timeAxis = useMemo((): AxisOptions<StatDatapoint> => {\n    return {\n      getValue: (datum) => new Date(datum.date),\n      hardMax: new Date(max + diff * 0.02),\n      hardMin: new Date(min - diff * 0.02),\n      tickCount: 6,\n      formatters: {\n        // scale: (value?: Date) => fmt_date(value ?? new Date()),\n        tooltip: (value?: Date) => (\n          <div className=\"text-lg font-mono\">\n            {fmt_utc_date(value ?? new Date())}\n          </div>\n        ),\n        cursor: (_value?: Date) => false,\n      },\n    };\n  }, [min, max, diff]);\n\n  // Determine the dynamic scaling for network-related types\n  const allValues = (seriesData ?? [{ data: stats ?? [] }]).flatMap((s) =>\n    s.data.map((d) => d.value)\n  );\n  const maxStatValue = Math.max(...(allValues.length ? allValues : [0]));\n\n  const { unit, maxUnitValue } = useMemo(() => {\n    if (type === \"Network Ingress\" || type === \"Network Egress\") {\n      if (maxStatValue <= BYTES_PER_KB) {\n        return { unit: \"KB\", maxUnitValue: BYTES_PER_KB };\n      } else if (maxStatValue <= BYTES_PER_MB) {\n        return { unit: \"MB\", maxUnitValue: BYTES_PER_MB };\n      } else if (maxStatValue <= BYTES_PER_GB) {\n        return { unit: \"GB\", maxUnitValue: BYTES_PER_GB };\n      } else {\n        return { unit: \"TB\", maxUnitValue: BYTES_PER_GB * 1024 }; // Larger scale for high values\n      }\n    }\n    if (type === \"Load Average\") {\n      // Leave unitless; set max slightly above observed\n      return {\n        unit: \"\",\n        maxUnitValue: maxStatValue === 0 ? 1 : maxStatValue * 1.2,\n      };\n    }\n    return { unit: \"\", maxUnitValue: 100 }; // Default for CPU, memory, disk\n  }, [type, maxStatValue]);\n\n  const valueAxis = useMemo(\n    (): AxisOptions<StatDatapoint>[] => [\n      {\n        getValue: (datum) => datum.value,\n        elementType: type === \"Load Average\" ? \"line\" : \"area\",\n        stacked: type !== \"Load Average\",\n        min: 0,\n        max: maxUnitValue,\n        formatters: {\n          tooltip: (value?: number) => (\n            <div className=\"text-lg font-mono\">\n              {(type === \"Network Ingress\" || type === \"Network Egress\") && unit\n                ? `${(value ?? 0) / (maxUnitValue / 1024)} ${unit}`\n                : type === \"Load Average\"\n                  ? `${(value ?? 0).toFixed(2)}`\n                  : `${value?.toFixed(2)}%`}\n            </div>\n          ),\n        },\n      },\n    ],\n    [type, maxUnitValue, unit]\n  );\n\n  if ((seriesData?.[0]?.data.length ?? 0) < 2) {\n    return (\n      <div className=\"w-full h-full flex gap-4 justify-center items-center\">\n        <OctagonAlert className=\"w-6 h-6\" />\n        <h1>Not enough data yet, choose a smaller interval.</h1>\n      </div>\n    );\n  }\n\n  return (\n    <Chart\n      options={{\n        data: seriesData ?? [{ label: type, data: stats ?? [] }],\n        primaryAxis: timeAxis,\n        secondaryAxes: valueAxis,\n        defaultColors:\n          type === \"Load Average\"\n            ? [\n                hex_color_by_intention(\"Good\"),\n                hex_color_by_intention(\"Neutral\"),\n                hex_color_by_intention(\"Unknown\"),\n              ]\n            : [getColor(type)],\n        dark: currentTheme === \"dark\",\n        padding: {\n          left: 10,\n          right: 10,\n        },\n        // tooltip: {\n        //   showDatumInTooltip: () => false,\n        // },\n      }}\n    />\n  );\n};\n\nconst getStat = (stat: Types.SystemStatsRecord, type: StatType) => {\n  if (type === \"Cpu\") return stat.cpu_perc || 0;\n  if (type === \"Memory\") return (100 * stat.mem_used_gb) / stat.mem_total_gb;\n  if (type === \"Disk\") return (100 * stat.disk_used_gb) / stat.disk_total_gb;\n  if (type === \"Network Ingress\") return stat.network_ingress_bytes || 0;\n  if (type === \"Network Egress\") return stat.network_egress_bytes || 0;\n  return 0;\n};\n\nconst getColor = (type: StatType) => {\n  if (type === \"Cpu\") return hex_color_by_intention(\"Good\");\n  if (type === \"Memory\") return hex_color_by_intention(\"Warning\");\n  if (type === \"Disk\") return hex_color_by_intention(\"Neutral\");\n  if (type === \"Network Ingress\") return hex_color_by_intention(\"Good\");\n  if (type === \"Network Egress\") return hex_color_by_intention(\"Critical\");\n  return hex_color_by_intention(\"Unknown\");\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/stats-mini.tsx",
    "content": "import { useRead } from \"@lib/hooks\";\nimport { cn } from \"@lib/utils\";\nimport { Progress } from \"@ui/progress\";\nimport { ServerState } from \"komodo_client/dist/types\";\nimport { Cpu, Database, MemoryStick, LucideIcon } from \"lucide-react\";\nimport { useMemo } from \"react\";\n\ninterface ServerStatsMiniProps {\n  id: string;\n  className?: string;\n  enabled?: boolean;\n}\n\ninterface StatItemProps {\n  icon: LucideIcon;\n  label: string;\n  percentage: number;\n  type: \"cpu\" | \"memory\" | \"disk\";\n  isUnreachable: boolean;\n  getTextColor: (percentage: number, type: \"cpu\" | \"memory\" | \"disk\") => string;\n}\n\nconst StatItem = ({ icon: Icon, label, percentage, type, isUnreachable, getTextColor }: StatItemProps) => (\n  <div className=\"flex items-center gap-2\">\n    <Icon className=\"w-3 h-3 text-muted-foreground\" aria-hidden=\"true\" />\n    <div className=\"flex-1 min-w-0\">\n      <div className=\"flex items-center justify-between pb-1\">\n        <span className=\"text-xs text-muted-foreground\">{label}</span>\n        <span\n          className={cn(\n            \"text-xs font-medium\",\n            isUnreachable ? \"text-muted-foreground\" : getTextColor(percentage, type)\n          )}\n        >\n          {isUnreachable ? \"N/A\" : `${percentage}%`}\n        </span>\n      </div>\n      <Progress\n        value={isUnreachable ? 0 : percentage}\n        className={cn(\"h-1\", \"[&>div]:transition-all\")}\n      />\n    </div>\n  </div>\n);\n\nexport const ServerStatsMini = ({ id, className, enabled = true }: ServerStatsMiniProps) => {\n  const calculatePercentage = (value: number) =>\n    Number((value ?? 0).toFixed(2));\n\n  const servers = useRead(\"ListServers\", {}).data;\n  const server = servers?.find((s) => s.id === id);\n\n  const isServerAvailable = server && \n    server.info.state !== ServerState.Disabled && \n    server.info.state !== ServerState.NotOk;\n  \n  const serverDetails = useRead(\"GetServer\", { server: id }, {\n    enabled: enabled && isServerAvailable\n  }).data;\n  \n  const cpuWarning = serverDetails?.config?.cpu_warning ?? 75;\n  const cpuCritical = serverDetails?.config?.cpu_critical ?? 90;\n  const memWarning = serverDetails?.config?.mem_warning ?? 75;\n  const memCritical = serverDetails?.config?.mem_critical ?? 90;\n  const diskWarning = serverDetails?.config?.disk_warning ?? 75;\n  const diskCritical = serverDetails?.config?.disk_critical ?? 90;\n\n  const getTextColor = (percentage: number, type: \"cpu\" | \"memory\" | \"disk\") => {\n    const warning = type === \"cpu\" ? cpuWarning : type === \"memory\" ? memWarning : diskWarning;\n    const critical = type === \"cpu\" ? cpuCritical : type === \"memory\" ? memCritical : diskCritical;\n    \n    if (percentage >= critical) return \"text-red-600\";\n    if (percentage >= warning) return \"text-yellow-600\";\n    return \"text-green-600\";\n  };\n  \n  const stats = useRead(\n    \"GetSystemStats\",\n    { server: id },\n    {\n      enabled: enabled && isServerAvailable,\n      refetchInterval: 15_000,\n      staleTime: 5_000,\n    },\n  ).data;\n\n  if (!server) {\n    return null;\n  }\n\n  const calculations = useMemo(() => {\n    const cpuPercentage = stats ? calculatePercentage(stats.cpu_perc) : 0;\n    const memoryPercentage = stats && stats.mem_total_gb > 0 ? calculatePercentage((stats.mem_used_gb / stats.mem_total_gb) * 100) : 0;\n\n    const diskUsed = stats ? stats.disks.reduce((acc, disk) => acc + disk.used_gb, 0) : 0;\n    const diskTotal = stats ? stats.disks.reduce((acc, disk) => acc + disk.total_gb, 0) : 0;\n    const diskPercentage = diskTotal > 0 ? calculatePercentage((diskUsed / diskTotal) * 100) : 0;\n      \n    const isUnreachable = !stats || server.info.state === ServerState.NotOk;\n    const isDisabled = server.info.state === ServerState.Disabled;\n    \n    return {\n      cpuPercentage,\n      memoryPercentage,\n      diskPercentage,\n      isUnreachable,\n      isDisabled\n    };\n  }, [stats, server.info.state]);\n\n  const { cpuPercentage, memoryPercentage, diskPercentage, isUnreachable, isDisabled } = calculations;\n  const overlayClass = (isUnreachable || isDisabled) ? \"opacity-50\" : \"\";\n\n  const statItems = useMemo(() => [\n    { icon: Cpu, label: \"CPU\", percentage: cpuPercentage, type: \"cpu\" as const },\n    { icon: MemoryStick, label: \"Memory\", percentage: memoryPercentage, type: \"memory\" as const },\n    { icon: Database, label: \"Disk\", percentage: diskPercentage, type: \"disk\" as const },\n  ], [cpuPercentage, memoryPercentage, diskPercentage]);\n\n  return (\n    <div className={cn(\"relative flex flex-col gap-2\", overlayClass, className)}>\n      {statItems.map((item) => (\n        <StatItem\n          key={item.label}\n          icon={item.icon}\n          label={item.label}\n          percentage={item.percentage}\n          type={item.type}\n          isUnreachable={isUnreachable || isDisabled}\n          getTextColor={getTextColor}\n        />\n      ))}\n      {isDisabled && (\n        <div className=\"absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-black/60 z-10\">\n          <span className=\"text-xs text-foreground font-bold italic text-center\">Disabled</span>\n        </div>\n      )}\n      {isUnreachable && !isDisabled && (\n        <div className=\"absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-black/60 z-10\">\n          <span className=\"text-xs text-foreground font-bold italic text-center\">Unreachable</span>\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/stats.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@ui/card\";\nimport { Progress } from \"@ui/progress\";\nimport { Cpu, Database, Loader2, MemoryStick, Search } from \"lucide-react\";\nimport { useLocalStorage, usePermissions, useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { ReactNode, useMemo, useState } from \"react\";\nimport { Input } from \"@ui/input\";\nimport { StatChart } from \"./stat-chart\";\nimport { useStatsGranularity } from \"./hooks\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { DockerResourceLink, ShowHideButton } from \"@components/util\";\nimport { filterBySplit } from \"@lib/utils\";\nimport { useIsServerAvailable } from \".\";\n\nexport const ServerStats = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther?: ReactNode;\n}) => {\n  const [interval, setInterval] = useStatsGranularity();\n\n  const { specific } = usePermissions({ type: \"Server\", id });\n  const isServerAvailable = useIsServerAvailable(id);\n\n  const stats = useRead(\n    \"GetSystemStats\",\n    { server: id },\n    { \n      enabled: isServerAvailable,\n      refetchInterval: 10_000 \n    }\n  ).data;\n  const info = useRead(\"GetSystemInformation\", { server: id }, { enabled: isServerAvailable }).data;\n\n  // Get all the containers with stats\n  const containers = useRead(\"ListDockerContainers\", {\n    server: id,\n  }, {\n    enabled: isServerAvailable\n  }).data?.filter((c) => c.stats);\n  const [showContainers, setShowContainers] = useLocalStorage(\n    \"stats-show-container-table-v1\",\n    true\n  );\n  const [containerSearch, setContainerSearch] = useState(\"\");\n  const filteredContainers = filterBySplit(\n    containers,\n    containerSearch,\n    (container) => container.name\n  );\n\n  const [showDisks, setShowDisks] = useLocalStorage(\n    \"stats-show-disks-table-v1\",\n    true\n  );\n  const disk_used = stats?.disks.reduce(\n    (acc, curr) => (acc += curr.used_gb),\n    0\n  );\n  const disk_total = stats?.disks.reduce(\n    (acc, curr) => (acc += curr.total_gb),\n    0\n  );\n\n  return (\n    <Section titleOther={titleOther}>\n      <div className=\"flex flex-col gap-8\">\n        {/* System Info */}\n        <Section title=\"System Info\">\n          <DataTable\n            tableKey=\"system-info\"\n            data={\n              info\n                ? [{ ...info, mem_total: stats?.mem_total_gb, disk_total }]\n                : []\n            }\n            columns={[\n              {\n                header: \"Hostname\",\n                accessorKey: \"host_name\",\n              },\n              {\n                header: \"Os\",\n                accessorKey: \"os\",\n              },\n              {\n                header: \"Kernel\",\n                accessorKey: \"kernel\",\n              },\n              {\n                header: \"CPU\",\n                accessorKey: \"cpu_brand\",\n              },\n              {\n                header: \"Core Count\",\n                accessorFn: ({ core_count }) =>\n                  `${core_count} Core${(core_count || 0) > 1 ? \"s\" : \"\"}`,\n              },\n              {\n                header: \"Total Memory\",\n                accessorFn: ({ mem_total }) => `${mem_total?.toFixed(2)} GB`,\n              },\n              {\n                header: \"Total Disk Size\",\n                accessorFn: ({ disk_total }) => `${disk_total?.toFixed(2)} GB`,\n              },\n            ]}\n          />\n        </Section>\n\n        {/* Current Overview */}\n        <Section title=\"Current\">\n          <div className=\"flex flex-col xl:flex-row gap-4\">\n            <LOAD_AVERAGE id={id} stats={stats} />\n            <CPU stats={stats} />\n            <RAM stats={stats} />\n            <DISK stats={stats} />\n            <NETWORK stats={stats} />\n          </div>\n        </Section>\n\n        {/* Container Breakdown */}\n        <Section\n          title=\"Containers\"\n          actions={\n            <div className=\"flex gap-4 items-center\">\n              <div className=\"relative\">\n                <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n                <Input\n                  value={containerSearch}\n                  onChange={(e) => setContainerSearch(e.target.value)}\n                  placeholder=\"search...\"\n                  className=\"pl-8 w-[200px] lg:w-[300px]\"\n                />\n              </div>\n              <ShowHideButton\n                show={showContainers}\n                setShow={setShowContainers}\n              />\n            </div>\n          }\n        >\n          {showContainers && (\n            <DataTable\n              tableKey=\"container-stats\"\n              data={filteredContainers}\n              columns={[\n                {\n                  accessorKey: \"name\",\n                  size: 200,\n                  header: ({ column }) => (\n                    <SortableHeader column={column} title=\"Name\" />\n                  ),\n                  cell: ({ row }) => (\n                    <DockerResourceLink\n                      type=\"container\"\n                      server_id={id}\n                      name={row.original.name}\n                    />\n                  ),\n                },\n                {\n                  accessorKey: \"stats.cpu_perc\",\n                  size: 100,\n                  header: ({ column }) => (\n                    <SortableHeader column={column} title=\"CPU\" />\n                  ),\n                },\n                {\n                  accessorKey: \"stats.mem_perc\",\n                  size: 200,\n                  header: ({ column }) => (\n                    <SortableHeader column={column} title=\"Memory\" />\n                  ),\n                  cell: ({ row }) => (\n                    <div className=\"flex items-center gap-2\">\n                      {row.original.stats?.mem_perc}\n                      <div className=\"text-muted-foreground text-sm\">\n                        ({row.original.stats?.mem_usage})\n                      </div>\n                    </div>\n                  ),\n                },\n                {\n                  accessorKey: \"stats.net_io\",\n                  size: 150,\n                  header: ({ column }) => (\n                    <SortableHeader column={column} title=\"Net I/O\" />\n                  ),\n                },\n                {\n                  accessorKey: \"stats.block_io\",\n                  size: 150,\n                  header: ({ column }) => (\n                    <SortableHeader column={column} title=\"Block I/O\" />\n                  ),\n                },\n                {\n                  accessorKey: \"stats.pids\",\n                  size: 100,\n                  header: ({ column }) => (\n                    <SortableHeader column={column} title=\"PIDs\" />\n                  ),\n                },\n              ]}\n            />\n          )}\n        </Section>\n\n        {/* Current Disk Breakdown */}\n        <Section\n          title=\"Disks\"\n          actions={\n            <div className=\"flex gap-4 items-center\">\n              <div className=\"flex gap-2 items-center\">\n                <div className=\"text-muted-foreground\">Used:</div>\n                {disk_used?.toFixed(2)} GB\n              </div>\n              <div className=\"flex gap-2 items-center\">\n                <div className=\"text-muted-foreground\">Total:</div>\n                {disk_total?.toFixed(2)} GB\n              </div>\n              <ShowHideButton show={showDisks} setShow={setShowDisks} />\n            </div>\n          }\n        >\n          {showDisks && (\n            <DataTable\n              sortDescFirst\n              tableKey=\"server-disks\"\n              data={\n                stats?.disks.map((disk) => ({\n                  ...disk,\n                  percentage: 100 * (disk.used_gb / disk.total_gb),\n                })) ?? []\n              }\n              columns={[\n                {\n                  header: \"Path\",\n                  cell: ({ row }) => (\n                    <div className=\"overflow-hidden overflow-ellipsis\">\n                      {row.original.mount}\n                    </div>\n                  ),\n                },\n                {\n                  accessorKey: \"used_gb\",\n                  header: ({ column }) => (\n                    <SortableHeader\n                      column={column}\n                      title=\"Used\"\n                      sortDescFirst\n                    />\n                  ),\n                  cell: ({ row }) => <>{row.original.used_gb.toFixed(2)} GB</>,\n                },\n                {\n                  accessorKey: \"total_gb\",\n                  header: ({ column }) => (\n                    <SortableHeader\n                      column={column}\n                      title=\"Total\"\n                      sortDescFirst\n                    />\n                  ),\n                  cell: ({ row }) => <>{row.original.total_gb.toFixed(2)} GB</>,\n                },\n                {\n                  accessorKey: \"percentage\",\n                  header: ({ column }) => (\n                    <SortableHeader\n                      column={column}\n                      title=\"Percentage\"\n                      sortDescFirst\n                    />\n                  ),\n                  cell: ({ row }) => (\n                    <>{row.original.percentage.toFixed(2)}% Full</>\n                  ),\n                },\n              ]}\n            />\n          )}\n        </Section>\n\n        {specific.includes(Types.SpecificPermission.Processes) && (\n          <Processes id={id} />\n        )}\n\n        {/* Historical Charts */}\n        <Section\n          title=\"Historical\"\n          actions={\n            <div className=\"flex gap-4 items-center\">\n              {/* Granularity Dropdown */}\n              <div className=\"flex items-center gap-2\">\n                <div className=\"text-muted-foreground\">Interval:</div>\n                <Select\n                  value={interval}\n                  onValueChange={(interval) =>\n                    setInterval(interval as Types.Timelength)\n                  }\n                >\n                  <SelectTrigger className=\"w-[150px]\">\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {[\n                      Types.Timelength.FifteenSeconds,\n                      Types.Timelength.ThirtySeconds,\n                      Types.Timelength.OneMinute,\n                      Types.Timelength.FiveMinutes,\n                      Types.Timelength.FifteenMinutes,\n                      Types.Timelength.ThirtyMinutes,\n                      Types.Timelength.OneHour,\n                      Types.Timelength.SixHours,\n                      Types.Timelength.OneDay,\n                    ].map((timelength) => (\n                      <SelectItem key={timelength} value={timelength}>\n                        {timelength}\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </div>\n            </div>\n          }\n        >\n          <div className=\"flex flex-col gap-8\">\n            <StatChart\n              server_id={id}\n              type=\"Load Average\"\n              className=\"w-full h-[250px]\"\n            />\n            <StatChart server_id={id} type=\"Cpu\" className=\"w-full h-[250px]\" />\n            <StatChart\n              server_id={id}\n              type=\"Memory\"\n              className=\"w-full h-[250px]\"\n            />\n            <StatChart\n              server_id={id}\n              type=\"Disk\"\n              className=\"w-full h-[250px]\"\n            />\n            <StatChart\n              server_id={id}\n              type=\"Network Ingress\"\n              className=\"w-full h-[250px]\"\n            />\n            <StatChart\n              server_id={id}\n              type=\"Network Egress\"\n              className=\"w-full h-[250px]\"\n            />\n          </div>\n        </Section>\n      </div>\n    </Section>\n  );\n};\n\nconst Processes = ({ id }: { id: string }) => {\n  const [show, setShow] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const searchSplit = search.toLowerCase().split(\" \");\n  return (\n    <Section\n      title=\"Processes\"\n      actions={\n        <div className=\"flex gap-4 items-center\">\n          <div className=\"relative\">\n            <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n            <Input\n              value={search}\n              onChange={(e) => setSearch(e.target.value)}\n              placeholder=\"search...\"\n              className=\"pl-8 w-[200px] lg:w-[300px]\"\n            />\n          </div>\n          <ShowHideButton show={show} setShow={setShow} />\n        </div>\n      }\n    >\n      {show && <ProcessesInner id={id} searchSplit={searchSplit} />}\n    </Section>\n  );\n};\n\nconst ProcessesInner = ({\n  id,\n  searchSplit,\n}: {\n  id: string;\n  searchSplit: string[];\n}) => {\n  const { data: processes, isPending } = useRead(\"ListSystemProcesses\", {\n    server: id,\n  });\n  const filtered = useMemo(\n    () =>\n      processes?.filter((process) => {\n        if (searchSplit.length === 0) return true;\n        const name = process.name.toLowerCase();\n        return searchSplit.every((search) => name.includes(search));\n      }),\n    [processes, searchSplit]\n  );\n  if (isPending)\n    return (\n      <div className=\"flex items-center justify-center h-[200px]\">\n        <Loader2 className=\"w-8 h-8 animate-spin\" />\n      </div>\n    );\n  if (!processes) return null;\n  return (\n    <DataTable\n      sortDescFirst\n      tableKey=\"server-processes\"\n      data={filtered ?? []}\n      columns={[\n        {\n          header: \"Name\",\n          accessorKey: \"name\",\n        },\n        {\n          header: \"Exe\",\n          accessorKey: \"exe\",\n          cell: ({ row }) => (\n            <div className=\"overflow-hidden overflow-ellipsis\">\n              {row.original.exe}\n            </div>\n          ),\n        },\n        {\n          accessorKey: \"cpu_perc\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Cpu\" sortDescFirst />\n          ),\n          cell: ({ row }) => <>{row.original.cpu_perc.toFixed(2)}%</>,\n        },\n        {\n          accessorKey: \"mem_mb\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Memory\" sortDescFirst />\n          ),\n          cell: ({ row }) => (\n            <>\n              {row.original.mem_mb > 1000\n                ? `${(row.original.mem_mb / 1024).toFixed(2)} GB`\n                : `${row.original.mem_mb.toFixed(2)} MB`}\n            </>\n          ),\n        },\n      ]}\n    />\n  );\n};\n\nconst StatBar = ({\n  title,\n  icon,\n  percentage,\n}: {\n  title: string;\n  icon: ReactNode;\n  percentage: number | undefined;\n}) => {\n  return (\n    <Card className=\"w-full\">\n      <CardHeader className=\"flex-row items-center justify-between\">\n        <CardTitle>{title}</CardTitle>\n        <div className=\"flex gap-2 items-center\">\n          <div className=\"text-lg\">{percentage?.toFixed(2)}%</div>\n          {icon}\n        </div>\n      </CardHeader>\n      <CardContent>\n        <Progress value={percentage} className=\"h-4\" />\n      </CardContent>\n    </Card>\n  );\n};\n\nconst CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => {\n  return (\n    <StatBar\n      title=\"CPU Usage\"\n      icon={<Cpu className=\"w-5 h-5\" />}\n      percentage={stats?.cpu_perc}\n    />\n  );\n};\n\nconst LOAD_AVERAGE = ({\n  id,\n  stats,\n}: {\n  id: string;\n  stats: Types.SystemStats | undefined;\n}) => {\n  if (!stats?.load_average) return null;\n  const { one = 0, five = 0, fifteen = 0 } = stats.load_average || {};\n  const isServerAvailable = useIsServerAvailable(id);\n  const cores = useRead(\"GetSystemInformation\", { server: id }, { enabled: isServerAvailable }).data?.core_count;\n\n  const pct = (load: number) =>\n    cores && cores > 0 ? Math.min((load / cores) * 100, 100) : undefined;\n  const textColor = (load: number) => {\n    const p = pct(load);\n    if (p === undefined) return \"text-muted-foreground\";\n    return p <= 50\n      ? \"text-green-600\"\n      : p <= 80\n        ? \"text-yellow-600\"\n        : \"text-red-600\";\n  };\n\n  return (\n    <Card className=\"w-full\">\n      <CardHeader className=\"pb-3\">\n        <div className=\"flex items-center justify-between\">\n          <CardTitle>Load Average</CardTitle>\n        </div>\n      </CardHeader>\n      <CardContent className=\"space-y-4\">\n        {/* Current Load */}\n        <div className=\"space-y-2\">\n          <div className=\"flex items-baseline justify-between\">\n            <span\n              className={`text-3xl font-bold tabular-nums ${textColor(one)}`}\n            >\n              {one.toFixed(2)}\n            </span>\n            <span className=\"text-sm text-muted-foreground\">\n              {cores && cores > 0\n                ? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores`\n                : \"N/A\"}\n            </span>\n          </div>\n          <Progress value={pct(one) ?? 0} className=\"h-2\" />\n        </div>\n\n        {/* Time Intervals */}\n        <div className=\"space-y-3\">\n          <div className=\"grid grid-cols-3 gap-4 text-sm\">\n            {[\n              [\"1m\", one],\n              [\"5m\", five],\n              [\"15m\", fifteen],\n            ].map(([label, value]) => (\n              <div className=\"space-y-1\" key={label as string}>\n                <div className=\"flex justify-between items-center\">\n                  <span className=\"text-muted-foreground\">{label}</span>\n                  <span\n                    className={`font-medium tabular-nums ${textColor(value as number)}`}\n                  >\n                    {(value as number).toFixed(2)}\n                  </span>\n                </div>\n                <Progress value={pct(value as number) ?? 0} className=\"h-1\" />\n              </div>\n            ))}\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n};\n\nconst RAM = ({ stats }: { stats: Types.SystemStats | undefined }) => {\n  const used = stats?.mem_used_gb;\n  const total = stats?.mem_total_gb;\n  return (\n    <StatBar\n      title=\"RAM Usage\"\n      icon={<MemoryStick className=\"w-5 h-5\" />}\n      percentage={((used ?? 0) / (total ?? 0)) * 100}\n    />\n  );\n};\n\nconst DISK = ({ stats }: { stats: Types.SystemStats | undefined }) => {\n  const used = stats?.disks.reduce((acc, curr) => (acc += curr.used_gb), 0);\n  const total = stats?.disks.reduce((acc, curr) => (acc += curr.total_gb), 0);\n  return (\n    <StatBar\n      title=\"Disk Usage\"\n      icon={<Database className=\"w-5 h-5\" />}\n      percentage={((used ?? 0) / (total ?? 0)) * 100}\n    />\n  );\n};\n\nconst formatBytes = (bytes: number) => {\n  const BYTES_PER_KB = 1024;\n  const BYTES_PER_MB = 1024 * BYTES_PER_KB;\n  const BYTES_PER_GB = 1024 * BYTES_PER_MB;\n\n  if (bytes >= BYTES_PER_GB) {\n    return { value: bytes / BYTES_PER_GB, unit: \"GB\" };\n  } else if (bytes >= BYTES_PER_MB) {\n    return { value: bytes / BYTES_PER_MB, unit: \"MB\" };\n  } else if (bytes >= BYTES_PER_KB) {\n    return { value: bytes / BYTES_PER_KB, unit: \"KB\" };\n  } else {\n    return { value: bytes, unit: \"bytes\" };\n  }\n};\n\nconst NETWORK = ({ stats }: { stats: Types.SystemStats | undefined }) => {\n  const ingress = stats?.network_ingress_bytes ?? 0;\n  const egress = stats?.network_egress_bytes ?? 0;\n\n  const formattedIngress = formatBytes(ingress);\n  const formattedEgress = formatBytes(egress);\n\n  return (\n    <Card className=\"w-full\">\n      <CardHeader className=\"flex-row justify-between\">\n        <CardTitle>Network Usage</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex justify-between items-center mb-4\">\n          <p className=\"font-medium\">Ingress</p>\n          <span className=\"text-sm text-gray-600\">\n            {formattedIngress.value.toFixed(2)} {formattedIngress.unit}\n          </span>\n        </div>\n        <div className=\"flex justify-between items-center\">\n          <p className=\"font-medium\">Egress</p>\n          <span className=\"text-sm text-gray-600\">\n            {formattedEgress.value.toFixed(2)} {formattedEgress.unit}\n          </span>\n        </div>\n      </CardContent>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/server/table.tsx",
    "content": "import { TableTags } from \"@components/tags\";\nimport { useRead, useSelectedResources } from \"@lib/hooks\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { ServerComponents, ServerVersion } from \".\";\nimport { ResourceLink } from \"../common\";\nimport { Types } from \"komodo_client\";\nimport { useCallback } from \"react\";\n\nexport const ServerTable = ({\n  servers,\n}: {\n  servers: Types.ServerListItem[];\n}) => {\n  const [_, setSelectedResources] = useSelectedResources(\"Server\");\n  const deployments = useRead(\"ListDeployments\", {}).data;\n  const stacks = useRead(\"ListStacks\", {}).data;\n  const repos = useRead(\"ListRepos\", {}).data;\n  const resourcesCount = useCallback(\n    (id: string) => {\n      return (\n        (deployments?.filter((d) => d.info.server_id === id).length || 0) +\n        (stacks?.filter((d) => d.info.server_id === id).length || 0) +\n        (repos?.filter((d) => d.info.server_id === id).length || 0)\n      );\n    },\n    [deployments, stacks, repos]\n  );\n  return (\n    <DataTable\n      tableKey=\"servers\"\n      data={servers}\n      selectOptions={{\n        selectKey: ({ name }) => name,\n        onSelect: setSelectedResources,\n      }}\n      columns={[\n        {\n          size: 250,\n          accessorKey: \"name\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Name\" />\n          ),\n          cell: ({ row }) => (\n            <ResourceLink type=\"Server\" id={row.original.id} />\n          ),\n        },\n        {\n          size: 100,\n          accessorKey: \"id\",\n          sortingFn: (a, b) => {\n            const sa = resourcesCount(a.original.id);\n            const sb = resourcesCount(b.original.id);\n\n            if (!sa && !sb) return 0;\n            if (!sa) return 1;\n            if (!sb) return -1;\n\n            if (sa > sb) return 1;\n            else if (sa < sb) return -1;\n            else return 0;\n          },\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Resources\" />\n          ),\n          cell: ({ row }) => {\n            return <>{resourcesCount(row.original.id)}</>;\n          },\n        },\n        {\n          size: 200,\n          accessorKey: \"info.region\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Region\" />\n          ),\n        },\n        {\n          size: 150,\n          accessorKey: \"info.version\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Version\" />\n          ),\n          cell: ({ row }) => <ServerVersion id={row.original.id} />,\n        },\n        {\n          size: 150,\n          accessorKey: \"info.state\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"State\" />\n          ),\n          cell: ({ row }) => <ServerComponents.State id={row.original.id} />,\n        },\n        {\n          header: \"Tags\",\n          cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/stack/actions.tsx",
    "content": "import { ActionWithDialog, ConfirmButton } from \"@components/util\";\nimport { useExecute, useRead } from \"@lib/hooks\";\nimport {\n  Download,\n  Pause,\n  Play,\n  RefreshCcw,\n  Rocket,\n  Square,\n  Trash,\n} from \"lucide-react\";\nimport { useStack } from \".\";\nimport { Types } from \"komodo_client\";\n\nexport const DeployStack = ({\n  id,\n  service,\n}: {\n  id: string;\n  service?: string;\n}) => {\n  const stack = useStack(id);\n  const state = stack?.info.state;\n  const { mutate: deploy, isPending } = useExecute(\"DeployStack\");\n  const deploying = useRead(\n    \"GetStackActionState\",\n    { stack: id },\n    { refetchInterval: 5000 }\n  ).data?.deploying;\n  const services = useRead(\"ListStackServices\", { stack: id }).data;\n  const container_state =\n    (service\n      ? services?.find((s) => s.service === service)?.container?.state\n      : undefined) ?? Types.ContainerStateStatusEnum.Empty;\n\n  if (!stack || state === Types.StackState.Unknown) {\n    return null;\n  }\n  const deployed =\n    state !== undefined &&\n    (service !== undefined\n      ? container_state !== Types.ContainerStateStatusEnum.Empty\n      : [\n          Types.StackState.Running,\n          Types.StackState.Paused,\n          Types.StackState.Stopped,\n          Types.StackState.Restarting,\n          Types.StackState.Unhealthy,\n        ].includes(state));\n\n  if (deployed) {\n    return (\n      <ActionWithDialog\n        name={`${stack?.name}${service ? ` - ${service}` : \"\"}`}\n        title=\"Redeploy\"\n        icon={<Rocket className=\"h-4 w-4\" />}\n        onClick={() =>\n          deploy({ stack: id, services: service ? [service] : [] })\n        }\n        disabled={isPending}\n        loading={isPending || deploying}\n      />\n    );\n  }\n\n  return (\n    <ConfirmButton\n      title=\"Deploy\"\n      icon={<Rocket className=\"w-4 h-4\" />}\n      onClick={() => deploy({ stack: id, services: service ? [service] : [] })}\n      disabled={isPending}\n      loading={isPending || deploying}\n    />\n  );\n};\n\nexport const DestroyStack = ({\n  id,\n  service,\n}: {\n  id: string;\n  service?: string;\n}) => {\n  const stack = useStack(id);\n  const state = stack?.info.state;\n  const { mutate: destroy, isPending } = useExecute(\"DestroyStack\");\n  const destroying = useRead(\n    \"GetStackActionState\",\n    { stack: id },\n    { refetchInterval: 5000 }\n  ).data?.destroying;\n  const services = useRead(\"ListStackServices\", { stack: id }).data;\n  const container_state =\n    (service\n      ? services?.find((s) => s.service === service)?.container?.state\n      : undefined) ?? Types.ContainerStateStatusEnum.Empty;\n\n  if (\n    !stack || service !== undefined\n      ? container_state === Types.ContainerStateStatusEnum.Empty\n      : state === undefined ||\n        [Types.StackState.Unknown, Types.StackState.Down].includes(state!)\n  ) {\n    return null;\n  }\n\n  return (\n    <ActionWithDialog\n      name={`${stack?.name}${service ? ` - ${service}` : \"\"}`}\n      title=\"Destroy\"\n      icon={<Trash className=\"h-4 w-4\" />}\n      onClick={() => destroy({ stack: id, services: service ? [service] : [] })}\n      disabled={isPending}\n      loading={isPending || destroying}\n    />\n  );\n};\n\nexport const PullStack = ({\n  id,\n  service,\n}: {\n  id: string;\n  service?: string;\n}) => {\n  const stack = useStack(id);\n  const { mutate: pull, isPending: pullPending } = useExecute(\"PullStack\");\n  const action_state = useRead(\n    \"GetStackActionState\",\n    { stack: id },\n    { refetchInterval: 5000 }\n  ).data;\n\n  if (!stack || (stack?.info.missing_files.length ?? 0) > 0) {\n    return null;\n  }\n\n  return (\n    <ConfirmButton\n      title={`Pull Image${service ? \"\" : \"s\"}`}\n      icon={<Download className=\"h-4 w-4\" />}\n      onClick={() => pull({ stack: id, services: service ? [service] : [] })}\n      disabled={pullPending}\n      loading={pullPending || action_state?.pulling}\n    />\n  );\n};\n\nexport const RestartStack = ({\n  id,\n  service,\n}: {\n  id: string;\n  service?: string;\n}) => {\n  const stack = useStack(id);\n  const state = stack?.info.state;\n  const { mutate: restart, isPending: restartPending } =\n    useExecute(\"RestartStack\");\n  const action_state = useRead(\n    \"GetStackActionState\",\n    { stack: id },\n    { refetchInterval: 5000 }\n  ).data;\n  const services = useRead(\"ListStackServices\", { stack: id }).data;\n  const container_state =\n    (service\n      ? services?.find((s) => s.service === service)?.container?.state\n      : undefined) ?? Types.ContainerStateStatusEnum.Empty;\n\n  if (\n    !stack ||\n    stack?.info.project_missing ||\n    (service && container_state !== Types.ContainerStateStatusEnum.Running) ||\n    // Only show if running or unhealthy\n    (state !== Types.StackState.Running && state !== Types.StackState.Unhealthy)\n  ) {\n    return null;\n  }\n\n  return (\n    <ActionWithDialog\n      name={`${stack?.name}${service ? ` - ${service}` : \"\"}`}\n      title=\"Restart\"\n      icon={<RefreshCcw className=\"h-4 w-4\" />}\n      onClick={() => restart({ stack: id, services: service ? [service] : [] })}\n      disabled={restartPending}\n      loading={restartPending || action_state?.restarting}\n    />\n  );\n};\n\nexport const StartStopStack = ({\n  id,\n  service,\n}: {\n  id: string;\n  service?: string;\n}) => {\n  const stack = useStack(id);\n  const state = stack?.info.state ?? Types.StackState.Unknown;\n  const { mutate: start, isPending: startPending } = useExecute(\"StartStack\");\n  const { mutate: stop, isPending: stopPending } = useExecute(\"StopStack\");\n  const action_state = useRead(\n    \"GetStackActionState\",\n    { stack: id },\n    { refetchInterval: 5000 }\n  ).data;\n  const services = useRead(\"ListStackServices\", { stack: id }).data;\n  const container_state =\n    (service\n      ? services?.find((s) => s.service === service)?.container?.state\n      : undefined) ?? Types.ContainerStateStatusEnum.Empty;\n\n  if (\n    !stack ||\n    [Types.StackState.Down, Types.StackState.Unknown].includes(state)\n  ) {\n    return null;\n  }\n\n  const showStart = service\n    ? ((container_state &&\n        container_state !== Types.ContainerStateStatusEnum.Running) ??\n      false)\n    : state !== Types.StackState.Running;\n  const showStop = service\n    ? ((container_state &&\n        container_state !== Types.ContainerStateStatusEnum.Exited) ??\n      false)\n    : state !== Types.StackState.Stopped;\n\n  return (\n    <>\n      {showStart && (\n        <ConfirmButton\n          title=\"Start\"\n          icon={<Play className=\"h-4 w-4\" />}\n          onClick={() =>\n            start({ stack: id, services: service ? [service] : [] })\n          }\n          disabled={startPending}\n          loading={startPending || action_state?.starting}\n        />\n      )}\n      {showStop && (\n        <ActionWithDialog\n          name={`${stack?.name}${service ? ` - ${service}` : \"\"}`}\n          title=\"Stop\"\n          icon={<Square className=\"h-4 w-4\" />}\n          onClick={() =>\n            stop({ stack: id, services: service ? [service] : [] })\n          }\n          disabled={stopPending}\n          loading={stopPending || action_state?.stopping}\n        />\n      )}\n    </>\n  );\n};\n\nexport const PauseUnpauseStack = ({\n  id,\n  service,\n}: {\n  id: string;\n  service?: string;\n}) => {\n  const stack = useStack(id);\n  const state = stack?.info.state;\n  const { mutate: unpause, isPending: unpausePending } =\n    useExecute(\"UnpauseStack\");\n  const { mutate: pause, isPending: pausePending } = useExecute(\"PauseStack\");\n  const action_state = useRead(\n    \"GetStackActionState\",\n    { stack: id },\n    { refetchInterval: 5000 }\n  ).data;\n  const services = useRead(\"ListStackServices\", { stack: id }).data;\n  const container_state =\n    (service\n      ? services?.find((s) => s.service === service)?.container?.state\n      : undefined) ?? Types.ContainerStateStatusEnum.Empty;\n\n  if (!stack || stack?.info.project_missing) {\n    return null;\n  }\n\n  if (\n    (service && container_state === Types.ContainerStateStatusEnum.Paused) ||\n    state === Types.StackState.Paused\n  ) {\n    return (\n      <ConfirmButton\n        title=\"Unpause\"\n        icon={<Play className=\"h-4 w-4\" />}\n        onClick={() =>\n          unpause({ stack: id, services: service ? [service] : [] })\n        }\n        disabled={unpausePending}\n        loading={unpausePending || action_state?.unpausing}\n      />\n    );\n  }\n  if (\n    (service && container_state === Types.ContainerStateStatusEnum.Running) ||\n    state === Types.StackState.Running\n  ) {\n    return (\n      <ActionWithDialog\n        name={`${stack?.name}${service ? ` - ${service}` : \"\"}`}\n        title=\"Pause\"\n        icon={<Pause className=\"h-4 w-4\" />}\n        onClick={() => pause({ stack: id, services: service ? [service] : [] })}\n        disabled={pausePending}\n        loading={pausePending || action_state?.pausing}\n      />\n    );\n  }\n};\n"
  },
  {
    "path": "frontend/src/components/resources/stack/config.tsx",
    "content": "import { Config, ConfigComponent } from \"@components/config\";\nimport {\n  AccountSelectorConfig,\n  AddExtraArgMenu,\n  ConfigItem,\n  ConfigList,\n  ConfigSwitch,\n  InputList,\n  ProviderSelectorConfig,\n  SystemCommand,\n  WebhookBuilder,\n} from \"@components/config/util\";\nimport { Types } from \"komodo_client\";\nimport {\n  getWebhookIntegration,\n  useInvalidate,\n  useLocalStorage,\n  usePermissions,\n  useRead,\n  useWebhookIdOrName,\n  useWebhookIntegrations,\n  useWrite,\n} from \"@lib/hooks\";\nimport { ReactNode, useState } from \"react\";\nimport { CopyWebhook, ResourceLink, ResourceSelector } from \"../common\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { SecretsSearch } from \"@components/config/env_vars\";\nimport { ConfirmButton, ShowHideButton } from \"@components/util\";\nimport { MonacoEditor } from \"@components/monaco\";\nimport { useToast } from \"@ui/use-toast\";\nimport { text_color_class_by_intention } from \"@lib/color\";\nimport {\n  Ban,\n  ChevronsUpDown,\n  CirclePlus,\n  MinusCircle,\n  PlusCircle,\n  SearchX,\n  X,\n} from \"lucide-react\";\nimport { LinkedRepoConfig } from \"@components/config/linked_repo\";\nimport { Button } from \"@ui/button\";\nimport { Input } from \"@ui/input\";\nimport { useStack } from \".\";\nimport { filterBySplit } from \"@lib/utils\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport { Checkbox } from \"@ui/checkbox\";\n\ntype StackMode = \"UI Defined\" | \"Files On Server\" | \"Git Repo\" | undefined;\nconst STACK_MODES: StackMode[] = [\"UI Defined\", \"Files On Server\", \"Git Repo\"];\n\nfunction getStackMode(\n  update: Partial<Types.StackConfig>,\n  config: Types.StackConfig\n): StackMode {\n  if (update.files_on_host ?? config.files_on_host) return \"Files On Server\";\n  if (\n    (update.linked_repo ?? config.linked_repo) ||\n    (update.repo ?? config.repo)\n  )\n    return \"Git Repo\";\n  if (update.file_contents ?? config.file_contents) return \"UI Defined\";\n  return undefined;\n}\n\nexport const StackConfig = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const [show, setShow] = useLocalStorage(`stack-${id}-show`, {\n    file: true,\n    env: true,\n    git: true,\n    webhooks: true,\n  });\n  const { canWrite } = usePermissions({ type: \"Stack\", id });\n  const stack = useRead(\"GetStack\", { stack: id }).data;\n  const config = stack?.config;\n  const name = stack?.name;\n  const webhooks = useRead(\"GetStackWebhooksEnabled\", { stack: id }).data;\n  const global_disabled =\n    useRead(\"GetCoreInfo\", {}).data?.ui_write_disabled ?? false;\n  const [update, set] = useLocalStorage<Partial<Types.StackConfig>>(\n    `stack-${id}-update-v1`,\n    {}\n  );\n  const { mutateAsync } = useWrite(\"UpdateStack\");\n  const { integrations } = useWebhookIntegrations();\n  const [id_or_name] = useWebhookIdOrName();\n\n  if (!config) return null;\n\n  const disabled = global_disabled || !canWrite;\n\n  const run_build = update.run_build ?? config.run_build;\n  const mode = getStackMode(update, config);\n\n  const git_provider = update.git_provider ?? config.git_provider;\n  const webhook_integration = getWebhookIntegration(integrations, git_provider);\n\n  const setMode = (mode: StackMode) => {\n    if (mode === \"Files On Server\") {\n      set({ ...update, files_on_host: true });\n    } else if (mode === \"Git Repo\") {\n      set({\n        ...update,\n        files_on_host: false,\n        repo: update.repo || config.repo || \"namespace/repo\",\n      });\n    } else if (mode === \"UI Defined\") {\n      set({\n        ...update,\n        files_on_host: false,\n        repo: \"\",\n        file_contents:\n          update.file_contents ||\n          config.file_contents ||\n          DEFAULT_STACK_FILE_CONTENTS,\n      });\n    } else if (mode === undefined) {\n      set({\n        ...update,\n        files_on_host: false,\n        repo: \"\",\n        file_contents: \"\",\n      });\n    }\n  };\n\n  let components: Record<\n    string,\n    false | ConfigComponent<Types.StackConfig>[] | undefined\n  > = {};\n\n  const server_component: ConfigComponent<Types.StackConfig> = {\n    label: \"Server\",\n    labelHidden: true,\n    components: {\n      server_id: (server_id, set) => {\n        return (\n          <ConfigItem\n            label={\n              server_id ? (\n                <div className=\"flex gap-3 text-lg font-bold\">\n                  Server:\n                  <ResourceLink type=\"Server\" id={server_id} />\n                </div>\n              ) : (\n                \"Select Server\"\n              )\n            }\n            description=\"Select the Server to deploy on.\"\n          >\n            <ResourceSelector\n              type=\"Server\"\n              selected={server_id}\n              onSelect={(server_id) => set({ server_id })}\n              disabled={disabled}\n              align=\"start\"\n            />\n          </ConfigItem>\n        );\n      },\n    },\n  };\n\n  const choose_mode: ConfigComponent<Types.StackConfig> = {\n    label: \"Choose Mode\",\n    labelHidden: true,\n    components: {\n      server_id: () => {\n        return (\n          <ConfigItem\n            label=\"Choose Mode\"\n            description=\"Will the file contents be defined in UI, stored on the server, or pulled from a git repo?\"\n            boldLabel\n          >\n            <Select\n              value={mode}\n              onValueChange={(mode) => setMode(mode as StackMode)}\n              disabled={disabled}\n            >\n              <SelectTrigger\n                className=\"w-[200px] capitalize\"\n                disabled={disabled}\n              >\n                <SelectValue placeholder=\"Select Mode\" />\n              </SelectTrigger>\n              <SelectContent>\n                {STACK_MODES.map((mode) => (\n                  <SelectItem\n                    key={mode}\n                    value={mode!}\n                    className=\"capitalize cursor-pointer\"\n                  >\n                    {mode}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </ConfigItem>\n        );\n      },\n    },\n  };\n\n  const environment: ConfigComponent<Types.StackConfig> = {\n    label: \"Environment\",\n    description: \"Pass these variables to the compose command\",\n    actions: (\n      <ShowHideButton\n        show={show.env}\n        setShow={(env) => setShow({ ...show, env })}\n      />\n    ),\n    contentHidden: !show.env,\n    components: {\n      environment: (env, set) => (\n        <div className=\"flex flex-col gap-4\">\n          <SecretsSearch server={update.server_id ?? config.server_id} />\n          <MonacoEditor\n            value={env || \"  # VARIABLE = value\\n\"}\n            onValueChange={(environment) => set({ environment })}\n            language=\"key_value\"\n            readOnly={disabled}\n          />\n        </div>\n      ),\n      env_file_path: {\n        description:\n          \"The path to write the file to, relative to the 'Run Directory'.\",\n        placeholder: \".env\",\n      },\n      additional_env_files:\n        (mode === \"Files On Server\" || mode === \"Git Repo\") &&\n        ((values, set) => (\n          <ConfigList\n            label=\"Additional Env Files\"\n            boldLabel\n            addLabel=\"Add Env File\"\n            description=\"Add additional env files to pass with '--env-file'. Relative to the 'Run Directory'.\"\n            field=\"additional_env_files\"\n            values={values ?? []}\n            set={set}\n            disabled={disabled}\n            placeholder=\".env\"\n          />\n        )),\n    },\n  };\n\n  const config_files: ConfigComponent<Types.StackConfig> = {\n    label: \"Config Files\",\n    description:\n      \"Add other config files to associate with the Stack, and edit in the UI. Relative to 'Run Directory'.\",\n    components: {\n      config_files: (value, set) => (\n        <ConfigFiles id={id} value={value} set={set} disabled={disabled} />\n      ),\n    },\n  };\n\n  const auto_update = update.auto_update ?? config.auto_update ?? false;\n\n  const general_common: ConfigComponent<Types.StackConfig>[] = [\n    {\n      label: \"Auto Update\",\n      components: {\n        poll_for_updates: (poll, set) => {\n          return (\n            <ConfigSwitch\n              label=\"Poll for Updates\"\n              description=\"Check for updates to the image on an interval.\"\n              value={auto_update || poll}\n              onChange={(poll_for_updates) => set({ poll_for_updates })}\n              disabled={disabled || auto_update}\n            />\n          );\n        },\n        auto_update: {\n          description: \"Trigger a redeploy if a newer image is found.\",\n        },\n        auto_update_all_services: (value, set) => {\n          return (\n            <ConfigSwitch\n              label=\"Full Stack Auto Update\"\n              description=\"Always redeploy full stack instead of just specific services with update.\"\n              value={value}\n              onChange={(auto_update_all_services) =>\n                set({ auto_update_all_services })\n              }\n              disabled={disabled || !auto_update}\n            />\n          );\n        },\n      },\n    },\n    {\n      label: \"Links\",\n      labelHidden: true,\n      components: {\n        links: (values, set) => (\n          <ConfigList\n            label=\"Links\"\n            boldLabel\n            addLabel=\"Add Link\"\n            description=\"Add quick links in the resource header\"\n            field=\"links\"\n            values={values ?? []}\n            set={set}\n            disabled={disabled}\n            placeholder=\"Input link\"\n          />\n        ),\n      },\n    },\n  ];\n\n  const advanced: ConfigComponent<Types.StackConfig>[] = [\n    {\n      label: \"Project Name\",\n      labelHidden: true,\n      components: {\n        project_name: {\n          placeholder: \"Compose project name\",\n          boldLabel: true,\n          description:\n            \"Optionally set a different compose project name. If importing existing stack, this should match the compose project name on your host.\",\n        },\n      },\n    },\n    {\n      label: \"Pre Deploy\",\n      description:\n        \"Execute a shell command before running docker compose up. The 'path' is relative to the Run Directory\",\n      components: {\n        pre_deploy: (value, set) => (\n          <SystemCommand\n            value={value}\n            set={(value) => set({ pre_deploy: value })}\n            disabled={disabled}\n          />\n        ),\n      },\n    },\n    {\n      label: \"Post Deploy\",\n      description:\n        \"Execute a shell command after running docker compose up. The 'path' is relative to the Run Directory\",\n      components: {\n        post_deploy: (value, set) => (\n          <SystemCommand\n            value={value}\n            set={(value) => set({ post_deploy: value })}\n            disabled={disabled}\n          />\n        ),\n      },\n    },\n    {\n      label: \"Extra Args\",\n      labelHidden: true,\n      components: {\n        extra_args: (value, set) => (\n          <ConfigItem\n            label=\"Extra Args\"\n            boldLabel\n            description=\"Add extra args inserted after 'docker compose up -d'\"\n          >\n            {!disabled && (\n              <AddExtraArgMenu\n                type=\"Stack\"\n                onSelect={(suggestion) =>\n                  set({\n                    extra_args: [\n                      ...(update.extra_args ?? config.extra_args ?? []),\n                      suggestion,\n                    ],\n                  })\n                }\n                disabled={disabled}\n              />\n            )}\n            <InputList\n              field=\"extra_args\"\n              values={value ?? []}\n              set={set}\n              disabled={disabled}\n              placeholder=\"--extra-arg=value\"\n            />\n          </ConfigItem>\n        ),\n      },\n    },\n    {\n      label: \"Ignore Services\",\n      labelHidden: true,\n      components: {\n        ignore_services: (values, set) => (\n          <ConfigList\n            label=\"Ignore Services\"\n            boldLabel\n            description=\"If your compose file has init services that exit early, ignore them here so your stack will report the correct health.\"\n            field=\"ignore_services\"\n            values={values ?? []}\n            set={set}\n            disabled={disabled}\n            placeholder=\"Input service name\"\n          />\n        ),\n      },\n    },\n    {\n      label: \"Pull Images\",\n      labelHidden: true,\n      components: {\n        registry_provider: (provider, set) => {\n          return (\n            <ProviderSelectorConfig\n              boldLabel\n              description=\"Login to a registry for private image access.\"\n              account_type=\"docker\"\n              selected={provider}\n              disabled={disabled}\n              onSelect={(registry_provider) => set({ registry_provider })}\n            />\n          );\n        },\n        registry_account: (value, set) => {\n          const server_id = update.server_id || config.server_id;\n          const provider = update.registry_provider ?? config.registry_provider;\n          if (!provider) {\n            return null;\n          }\n          return (\n            <AccountSelectorConfig\n              id={server_id}\n              type={server_id ? \"Server\" : \"None\"}\n              account_type=\"docker\"\n              provider={provider}\n              selected={value}\n              onSelect={(registry_account) => set({ registry_account })}\n              disabled={disabled}\n              placeholder=\"None\"\n            />\n          );\n        },\n        auto_pull: {\n          label: \"Pre Pull Images\",\n          description:\n            \"Ensure 'docker compose pull' is run before redeploying the Stack. Otherwise, use 'pull_policy' in docker compose file.\",\n        },\n      },\n    },\n    {\n      label: \"Build Images\",\n      labelHidden: true,\n      components: {\n        run_build: {\n          label: \"Pre Build Images\",\n          description:\n            \"Ensure 'docker compose build' is run before redeploying the Stack. Otherwise, can use '--build' as an Extra Arg.\",\n        },\n        build_extra_args: (value, set) =>\n          run_build && (\n            <ConfigItem\n              label=\"Build Extra Args\"\n              description=\"Add extra args inserted after 'docker compose build'\"\n            >\n              {!disabled && (\n                <AddExtraArgMenu\n                  type=\"StackBuild\"\n                  onSelect={(suggestion) =>\n                    set({\n                      build_extra_args: [\n                        ...(update.build_extra_args ??\n                          config.build_extra_args ??\n                          []),\n                        suggestion,\n                      ],\n                    })\n                  }\n                  disabled={disabled}\n                />\n              )}\n              <InputList\n                field=\"build_extra_args\"\n                values={value ?? []}\n                set={set}\n                disabled={disabled}\n                placeholder=\"--extra-arg=value\"\n              />\n            </ConfigItem>\n          ),\n      },\n    },\n    {\n      label: \"Destroy\",\n      labelHidden: true,\n      components: {\n        destroy_before_deploy: {\n          label: \"Destroy Before Deploy\",\n          description:\n            \"Ensure 'docker compose down' is run before redeploying the Stack.\",\n        },\n      },\n    },\n  ];\n\n  if (mode === undefined) {\n    components = {\n      \"\": [server_component, choose_mode],\n    };\n  } else if (mode === \"Files On Server\") {\n    components = {\n      \"\": [\n        server_component,\n        {\n          label: \"Files\",\n          components: {\n            run_directory: {\n              label: \"Run Directory\",\n              description: `Set the working directory when running the 'compose up' command. Can be absolute path, or relative to $PERIPHERY_STACK_DIR/${stack.name}`,\n              placeholder: \"/path/to/folder\",\n            },\n            file_paths: (value, set) => (\n              <ConfigList\n                label=\"File Paths\"\n                description=\"Add files to include using 'docker compose -f'. If empty, uses 'compose.yaml'. Relative to 'Run Directory'.\"\n                field=\"file_paths\"\n                values={value ?? []}\n                set={set}\n                disabled={disabled}\n                placeholder=\"compose.yaml\"\n              />\n            ),\n          },\n        },\n        environment,\n        config_files,\n        ...general_common,\n      ],\n      advanced,\n    };\n  } else if (mode === \"Git Repo\") {\n    const repo_linked = !!(update.linked_repo ?? config.linked_repo);\n    components = {\n      \"\": [\n        server_component,\n        {\n          label: \"Source\",\n          contentHidden: !show.git,\n          actions: (\n            <ShowHideButton\n              show={show.git}\n              setShow={(git) => setShow({ ...show, git })}\n            />\n          ),\n          components: {\n            linked_repo: (linked_repo, set) => (\n              <LinkedRepoConfig\n                linked_repo={linked_repo}\n                repo_linked={repo_linked}\n                set={set}\n                disabled={disabled}\n              />\n            ),\n            ...(!repo_linked\n              ? {\n                  git_provider: (provider, set) => {\n                    const https = update.git_https ?? config.git_https;\n                    return (\n                      <ProviderSelectorConfig\n                        account_type=\"git\"\n                        selected={provider}\n                        disabled={disabled}\n                        onSelect={(git_provider) => set({ git_provider })}\n                        https={https}\n                        onHttpsSwitch={() => set({ git_https: !https })}\n                      />\n                    );\n                  },\n                  git_account: (value, set) => {\n                    const server_id = update.server_id || config.server_id;\n                    return (\n                      <AccountSelectorConfig\n                        id={server_id}\n                        type={server_id ? \"Server\" : \"None\"}\n                        account_type=\"git\"\n                        provider={update.git_provider ?? config.git_provider}\n                        selected={value}\n                        onSelect={(git_account) => set({ git_account })}\n                        disabled={disabled}\n                        placeholder=\"None\"\n                      />\n                    );\n                  },\n                  repo: {\n                    placeholder: \"Enter repo\",\n                    description:\n                      \"The repo path on the provider. {namespace}/{repo_name}\",\n                  },\n                  branch: {\n                    placeholder: \"Enter branch\",\n                    description:\n                      \"Select a custom branch, or default to 'main'.\",\n                  },\n                  commit: {\n                    label: \"Commit Hash\",\n                    placeholder: \"Input commit hash\",\n                    description:\n                      \"Optional. Switch to a specific commit hash after cloning the branch.\",\n                  },\n                  clone_path: {\n                    placeholder: \"/clone/path/on/host\",\n                    description: (\n                      <div className=\"flex flex-col gap-0\">\n                        <div>\n                          Explicitly specify the folder on the host to clone the\n                          repo in.\n                        </div>\n                        <div>\n                          If <span className=\"font-bold\">relative</span> (no\n                          leading '/'), relative to{\" \"}\n                          {\"$root_directory/stacks/\" + stack.name}\n                        </div>\n                      </div>\n                    ),\n                  },\n                }\n              : {}),\n            reclone: {\n              description:\n                \"Delete the repo folder and clone it again, instead of using 'git pull'.\",\n            },\n          },\n        },\n        {\n          label: \"Files\",\n          components: {\n            run_directory: {\n              description:\n                \"Set the working directory when running the compose up command, relative to the root of the repo.\",\n              placeholder: \"path/to/folder\",\n            },\n            file_paths: (value, set) => (\n              <ConfigList\n                label=\"File Paths\"\n                description=\"Add files to include using 'docker compose -f'. If empty, uses 'compose.yaml'. Relative to 'Run Directory'.\"\n                field=\"file_paths\"\n                values={value ?? []}\n                set={set}\n                disabled={disabled}\n                placeholder=\"compose.yaml\"\n              />\n            ),\n          },\n        },\n        environment,\n        config_files,\n        ...general_common,\n        {\n          label: \"Webhooks\",\n          description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,\n          actions: (\n            <ShowHideButton\n              show={show.webhooks}\n              setShow={(webhooks) => setShow({ ...show, webhooks })}\n            />\n          ),\n          contentHidden: !show.webhooks,\n          components: {\n            [\"Guard\" as any]: () => {\n              if (update.branch ?? config.branch) {\n                return null;\n              }\n              return (\n                <ConfigItem label=\"Configure Branch\">\n                  <div>Must configure Branch before webhooks will work.</div>\n                </ConfigItem>\n              );\n            },\n            [\"Builder\" as any]: () => (\n              <WebhookBuilder git_provider={git_provider} />\n            ),\n            [\"Deploy\" as any]: () =>\n              (update.branch ?? config.branch) && (\n                <ConfigItem label=\"Webhook Url - Deploy\">\n                  <CopyWebhook\n                    integration={webhook_integration}\n                    path={`/stack/${id_or_name === \"Id\" ? id : encodeURIComponent(name ?? \"...\")}/deploy`}\n                  />\n                </ConfigItem>\n              ),\n            webhook_force_deploy: {\n              description:\n                \"Usually the Stack won't deploy unless there are changes to the files. Use this to force deploy.\",\n            },\n            webhook_enabled:\n              !!(update.branch ?? config.branch) &&\n              webhooks !== undefined &&\n              !webhooks.managed,\n            webhook_secret: {\n              description:\n                \"Provide a custom webhook secret for this resource, or use the global default.\",\n              placeholder: \"Input custom secret\",\n            },\n            [\"managed\" as any]: () => {\n              const inv = useInvalidate();\n              const { toast } = useToast();\n              const { mutate: createWebhook, isPending: createPending } =\n                useWrite(\"CreateStackWebhook\", {\n                  onSuccess: () => {\n                    toast({ title: \"Webhook Created\" });\n                    inv([\"GetStackWebhooksEnabled\", { stack: id }]);\n                  },\n                });\n              const { mutate: deleteWebhook, isPending: deletePending } =\n                useWrite(\"DeleteStackWebhook\", {\n                  onSuccess: () => {\n                    toast({ title: \"Webhook Deleted\" });\n                    inv([\"GetStackWebhooksEnabled\", { stack: id }]);\n                  },\n                });\n\n              if (\n                !(update.branch ?? config.branch) ||\n                !webhooks ||\n                !webhooks.managed\n              ) {\n                return null;\n              }\n\n              return (\n                <ConfigItem label=\"Manage Webhook\">\n                  {webhooks.deploy_enabled && (\n                    <div className=\"flex items-center gap-4 flex-wrap\">\n                      <div className=\"flex items-center gap-2\">\n                        Incoming webhook is{\" \"}\n                        <div className={text_color_class_by_intention(\"Good\")}>\n                          ENABLED\n                        </div>\n                        and will trigger\n                        <div\n                          className={text_color_class_by_intention(\"Neutral\")}\n                        >\n                          DEPLOY\n                        </div>\n                      </div>\n                      <ConfirmButton\n                        title=\"Disable\"\n                        icon={<Ban className=\"w-4 h-4\" />}\n                        variant=\"destructive\"\n                        onClick={() =>\n                          deleteWebhook({\n                            stack: id,\n                            action: Types.StackWebhookAction.Deploy,\n                          })\n                        }\n                        loading={deletePending}\n                        disabled={disabled || deletePending}\n                      />\n                    </div>\n                  )}\n                  {!webhooks.deploy_enabled && webhooks.refresh_enabled && (\n                    <div className=\"flex items-center gap-4 flex-wrap\">\n                      <div className=\"flex items-center gap-2\">\n                        Incoming webhook is{\" \"}\n                        <div className={text_color_class_by_intention(\"Good\")}>\n                          ENABLED\n                        </div>\n                        and will trigger\n                        <div\n                          className={text_color_class_by_intention(\"Neutral\")}\n                        >\n                          REFRESH\n                        </div>\n                      </div>\n                      <ConfirmButton\n                        title=\"Disable\"\n                        icon={<Ban className=\"w-4 h-4\" />}\n                        variant=\"destructive\"\n                        onClick={() =>\n                          deleteWebhook({\n                            stack: id,\n                            action: Types.StackWebhookAction.Refresh,\n                          })\n                        }\n                        loading={deletePending}\n                        disabled={disabled || deletePending}\n                      />\n                    </div>\n                  )}\n                  {!webhooks.deploy_enabled && !webhooks.refresh_enabled && (\n                    <div className=\"flex items-center gap-4 flex-wrap\">\n                      <div className=\"flex items-center gap-2\">\n                        Incoming webhook is{\" \"}\n                        <div\n                          className={text_color_class_by_intention(\"Critical\")}\n                        >\n                          DISABLED\n                        </div>\n                      </div>\n                      <ConfirmButton\n                        title=\"Enable Deploy\"\n                        icon={<CirclePlus className=\"w-4 h-4\" />}\n                        onClick={() =>\n                          createWebhook({\n                            stack: id,\n                            action: Types.StackWebhookAction.Deploy,\n                          })\n                        }\n                        loading={createPending}\n                        disabled={disabled || createPending}\n                      />\n                      <ConfirmButton\n                        title=\"Enable Refresh\"\n                        icon={<CirclePlus className=\"w-4 h-4\" />}\n                        onClick={() =>\n                          createWebhook({\n                            stack: id,\n                            action: Types.StackWebhookAction.Refresh,\n                          })\n                        }\n                        loading={createPending}\n                        disabled={disabled || createPending}\n                      />\n                    </div>\n                  )}\n                </ConfigItem>\n              );\n            },\n          },\n        },\n      ],\n      advanced,\n    };\n  } else if (mode === \"UI Defined\") {\n    components = {\n      \"\": [\n        server_component,\n        {\n          label: \"Compose File\",\n          description: \"Manage the compose file contents here.\",\n          actions: (\n            <ShowHideButton\n              show={show.file}\n              setShow={(file) => setShow({ ...show, file })}\n            />\n          ),\n          contentHidden: !show.file,\n          components: {\n            file_contents: (file_contents, set) => {\n              const show_default =\n                !file_contents &&\n                update.file_contents === undefined &&\n                !(update.repo ?? config.repo);\n              return (\n                <div className=\"flex flex-col gap-4\">\n                  <SecretsSearch />\n                  <MonacoEditor\n                    value={\n                      show_default ? DEFAULT_STACK_FILE_CONTENTS : file_contents\n                    }\n                    filename=\"compose.yaml\"\n                    onValueChange={(file_contents) => set({ file_contents })}\n                    language=\"yaml\"\n                    readOnly={disabled}\n                  />\n                </div>\n              );\n            },\n          },\n        },\n        environment,\n        ...general_common,\n      ],\n      advanced,\n    };\n  }\n\n  return (\n    <Config\n      titleOther={titleOther}\n      disabled={disabled}\n      original={config}\n      update={update}\n      set={set}\n      onSave={async () => {\n        await mutateAsync({ id, config: update });\n      }}\n      components={components}\n      file_contents_language=\"yaml\"\n    />\n  );\n};\n\nexport const DEFAULT_STACK_FILE_CONTENTS = `## Add your compose file here\nservices:\n  hello_world:\n    image: hello-world\n    # networks:\n    #   - default\n    # ports:\n    #   - 3000:3000\n    # volumes:\n    #   - data:/data\n\n# networks:\n#   default: {}\n\n# volumes:\n#   data:\n`;\n\nconst ConfigFiles = ({\n  id,\n  value,\n  set,\n  disabled,\n}: {\n  id: string;\n  value: Types.StackFileDependency[] | undefined;\n  set: (value: Partial<Types.StackConfig>) => void;\n  disabled: boolean;\n}) => {\n  const values = value ?? [];\n  return (\n    <ConfigItem>\n      {!disabled && (\n        <Button\n          variant=\"secondary\"\n          onClick={() =>\n            set({\n              config_files: [\n                ...values,\n                {\n                  path: \"\",\n                  services: [],\n                  requires: Types.StackFileRequires.None,\n                },\n              ],\n            })\n          }\n          className=\"flex items-center gap-2 w-[200px]\"\n        >\n          <PlusCircle className=\"w-4 h-4\" />\n          Add Additional File\n        </Button>\n      )}\n      {values.length > 0 && (\n        <div className=\"flex w-full\">\n          <div className=\"flex flex-col gap-4 w-fit\">\n            {values.map(({ path, services, requires }, i) => (\n              <div className=\"w-full flex flex-wrap gap-4\" key={i}>\n                <Input\n                  placeholder=\"configs/config.yaml\"\n                  value={path}\n                  onChange={(e) => {\n                    values[i] = { ...values[i], path: e.target.value };\n                    set({ config_files: [...values] });\n                  }}\n                  disabled={disabled}\n                  className=\"w-[400px] max-w-full\"\n                />\n\n                {!disabled && (\n                  <Button\n                    variant=\"secondary\"\n                    onClick={() =>\n                      set({\n                        config_files: [...values.filter((_, idx) => idx !== i)],\n                      })\n                    }\n                  >\n                    <MinusCircle className=\"w-4 h-4\" />\n                  </Button>\n                )}\n\n                <ServicesSelector\n                  id={id}\n                  selected_services={services ?? []}\n                  set={(services) => {\n                    values[i] = { ...values[i], services };\n                    set({ config_files: [...values] });\n                  }}\n                  disabled={disabled}\n                />\n\n                <RequiresSelector\n                  requires={requires ?? Types.StackFileRequires.None}\n                  set={(requires) => {\n                    values[i] = { ...values[i], requires };\n                    set({ config_files: [...values] });\n                  }}\n                  disabled={disabled}\n                />\n              </div>\n            ))}\n          </div>\n        </div>\n      )}\n    </ConfigItem>\n  );\n};\n\nconst ServicesSelector = ({\n  id,\n  selected_services,\n  set,\n  disabled,\n}: {\n  id: string;\n  selected_services: string[];\n  set: (services: string[]) => void;\n  disabled: boolean;\n}) => {\n  const services = useStack(id)?.info.services.map((s) => s.service) ?? [];\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n\n  const filtered = filterBySplit(services, search, (i) => i).sort();\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"secondary\"\n          className=\"flex justify-between gap-2 w-fit max-w-[350px]\"\n          disabled={disabled}\n        >\n          <div className=\"flex gap-2 items-center\">\n            <div className=\"text-xs text-muted-foreground\">Services:</div>\n            {selected_services.length === 0\n              ? \"All\"\n              : selected_services.join(\", \")}\n          </div>\n          {!disabled && <ChevronsUpDown className=\"w-3 h-3\" />}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[300px] max-h-[300px] p-0\">\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Search services\"\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center pt-3 pb-2\">\n              No services found\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n\n            <CommandGroup>\n              {filtered.map((service) => (\n                <CommandItem\n                  key={service}\n                  onSelect={() => {\n                    if (selected_services.includes(service)) {\n                      set(selected_services.filter((s) => s !== service));\n                    } else {\n                      set([...selected_services, service].sort());\n                    }\n                    // setOpen(false);\n                  }}\n                  className=\"flex items-center gap-2 cursor-pointer\"\n                >\n                  <Checkbox checked={selected_services.includes(service)} />\n                  <div className=\"p-1\">{service}</div>\n                </CommandItem>\n              ))}\n              {!search && selected_services.length > 0 && (\n                <CommandItem\n                  onSelect={() => {\n                    set([]);\n                    setOpen(false);\n                  }}\n                  className=\"flex items-center gap-2 cursor-pointer\"\n                  disabled={services.length === 0}\n                >\n                  <Button\n                    variant=\"destructive\"\n                    className=\"px-1 py-0 h-fit\"\n                    disabled={services.length === 0}\n                  >\n                    <X className=\"w-4\" />\n                  </Button>\n                  <div className=\"p-1\">Clear</div>\n                </CommandItem>\n              )}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nconst RequiresSelector = ({\n  requires,\n  set,\n  disabled,\n}: {\n  requires: Types.StackFileRequires;\n  set: (requires: Types.StackFileRequires) => void;\n  disabled: boolean;\n}) => {\n  return (\n    <Select\n      value={requires}\n      onValueChange={(requires: Types.StackFileRequires) => {\n        set(requires);\n      }}\n      disabled={disabled}\n    >\n      <SelectTrigger\n        className=\"w-[180px] flex gap-2 items-center\"\n        disabled={disabled}\n      >\n        <div className=\"text-xs text-muted-foreground\">Requires:</div>\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {Object.values(Types.StackFileRequires).map((requires) => (\n          <SelectItem key={requires} value={requires}>\n            {requires}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/stack/index.tsx",
    "content": "import {\n  useInvalidate,\n  useLocalStorage,\n  usePermissions,\n  useRead,\n  useWrite,\n} from \"@lib/hooks\";\nimport { RequiredResourceComponents } from \"@types\";\nimport { Card } from \"@ui/card\";\nimport {\n  CircleArrowUp,\n  Layers,\n  Loader2,\n  RefreshCcw,\n  Server,\n} from \"lucide-react\";\nimport {\n  DeleteResource,\n  NewResource,\n  ResourceLink,\n  ResourcePageHeader,\n  StandardSource,\n} from \"../common\";\nimport { StackTable } from \"./table\";\nimport {\n  border_color_class_by_intention,\n  stack_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\nimport { cn } from \"@lib/utils\";\nimport { useServer } from \"../server\";\nimport { Types } from \"komodo_client\";\nimport {\n  DeployStack,\n  DestroyStack,\n  PauseUnpauseStack,\n  PullStack,\n  RestartStack,\n  StartStopStack,\n} from \"./actions\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@ui/tabs\";\nimport { StackInfo } from \"./info\";\nimport { Badge } from \"@ui/badge\";\nimport { Button } from \"@ui/button\";\nimport { useToast } from \"@ui/use-toast\";\nimport { StackServices } from \"./services\";\nimport { DashboardPieChart } from \"@pages/home/dashboard\";\nimport { StatusBadge } from \"@components/util\";\nimport { StackConfig } from \"./config\";\nimport { GroupActions } from \"@components/group-actions\";\nimport { StackLogs } from \"./log\";\nimport { Tooltip, TooltipTrigger, TooltipContent } from \"@ui/tooltip\";\n\nexport const useStack = (id?: string) =>\n  useRead(\"ListStacks\", {}, { refetchInterval: 10_000 }).data?.find(\n    (d) => d.id === id\n  );\n\nexport const useFullStack = (id: string) =>\n  useRead(\"GetStack\", { stack: id }, { refetchInterval: 10_000 }).data;\n\nconst StackIcon = ({ id, size }: { id?: string; size: number }) => {\n  const state = useStack(id)?.info.state;\n  const color = stroke_color_class_by_intention(stack_state_intention(state));\n  return <Layers className={cn(`w-${size} h-${size}`, state && color)} />;\n};\n\nconst ConfigInfoServicesLog = ({ id }: { id: string }) => {\n  const [_view, setView] = useLocalStorage<\n    \"Config\" | \"Info\" | \"Services\" | \"Log\"\n  >(\"stack-tabs-v1\", \"Config\");\n  const info = useStack(id)?.info;\n  const { specific } = usePermissions({ type: \"Stack\", id });\n\n  const state = info?.state;\n  const hideInfo = !info?.files_on_host && !info?.repo && !info?.linked_repo;\n  const hideServices =\n    state === undefined ||\n    state === Types.StackState.Unknown ||\n    state === Types.StackState.Down;\n  const hideLogs =\n    hideServices || !specific.includes(Types.SpecificPermission.Logs);\n\n  const view =\n    (_view === \"Info\" && hideInfo) ||\n    (_view === \"Services\" && hideServices) ||\n    (_view === \"Log\" && hideLogs)\n      ? \"Config\"\n      : _view;\n\n  const title = (\n    <TabsList className=\"justify-start w-fit\">\n      <TabsTrigger value=\"Config\" className=\"w-[110px]\">\n        Config\n      </TabsTrigger>\n      <TabsTrigger\n        value=\"Info\"\n        className={cn(\"w-[110px]\", hideInfo && \"hidden\")}\n        disabled={hideInfo}\n      >\n        Info\n      </TabsTrigger>\n      <TabsTrigger\n        value=\"Services\"\n        className=\"w-[110px]\"\n        disabled={hideServices}\n      >\n        Services\n      </TabsTrigger>\n      {specific.includes(Types.SpecificPermission.Logs) && (\n        <TabsTrigger value=\"Log\" className=\"w-[110px]\" disabled={hideLogs}>\n          Log\n        </TabsTrigger>\n      )}\n    </TabsList>\n  );\n  return (\n    <Tabs value={view} onValueChange={setView as any}>\n      <TabsContent value=\"Config\">\n        <StackConfig id={id} titleOther={title} />\n      </TabsContent>\n      <TabsContent value=\"Info\">\n        <StackInfo id={id} titleOther={title} />\n      </TabsContent>\n      <TabsContent value=\"Services\">\n        <StackServices id={id} titleOther={title} />\n      </TabsContent>\n      <TabsContent value=\"Log\">\n        <StackLogs id={id} titleOther={title} />\n      </TabsContent>\n    </Tabs>\n  );\n};\n\nexport const StackComponents: RequiredResourceComponents = {\n  list_item: (id) => useStack(id),\n  resource_links: (resource) => (resource.config as Types.StackConfig).links,\n\n  Description: () => <>Deploy docker compose files.</>,\n\n  Dashboard: () => {\n    const summary = useRead(\"GetStacksSummary\", {}).data;\n    const all = [\n      summary?.running ?? 0,\n      summary?.stopped ?? 0,\n      summary?.unhealthy ?? 0,\n      summary?.unknown ?? 0,\n    ];\n    const [running, stopped, unhealthy, unknown] = all;\n    return (\n      <DashboardPieChart\n        data={[\n          all.every((item) => item === 0) && {\n            title: \"Down\",\n            intention: \"Neutral\",\n            value: summary?.down ?? 0,\n          },\n          { intention: \"Good\", value: running, title: \"Running\" },\n          {\n            intention: \"Warning\",\n            value: stopped,\n            title: \"Stopped\",\n          },\n          {\n            intention: \"Critical\",\n            value: unhealthy,\n            title: \"Unhealthy\",\n          },\n          {\n            intention: \"Unknown\",\n            value: unknown,\n            title: \"Unknown\",\n          },\n        ]}\n      />\n    );\n  },\n\n  GroupActions: () => (\n    <GroupActions\n      type=\"Stack\"\n      actions={[\n        \"PullStack\",\n        \"DeployStack\",\n        \"RestartStack\",\n        \"StopStack\",\n        \"DestroyStack\",\n      ]}\n    />\n  ),\n\n  New: ({ server_id: _server_id }) => {\n    const servers = useRead(\"ListServers\", {}).data;\n    const server_id = _server_id\n      ? _server_id\n      : servers && servers.length === 1\n        ? servers[0].id\n        : undefined;\n    return <NewResource type=\"Stack\" server_id={server_id} />;\n  },\n\n  Table: ({ resources }) => (\n    <StackTable stacks={resources as Types.StackListItem[]} />\n  ),\n\n  Icon: ({ id }) => <StackIcon id={id} size={4} />,\n  BigIcon: ({ id }) => <StackIcon id={id} size={8} />,\n\n  State: ({ id }) => {\n    const state = useStack(id)?.info.state ?? Types.StackState.Unknown;\n    return <StatusBadge text={state} intent={stack_state_intention(state)} />;\n  },\n\n  Info: {\n    Server: ({ id }) => {\n      const info = useStack(id)?.info;\n      const server = useServer(info?.server_id);\n      return server?.id ? (\n        <ResourceLink type=\"Server\" id={server?.id} />\n      ) : (\n        <div className=\"flex gap-2 items-center\">\n          <Server className=\"w-4 h-4\" />\n          <div>Unknown Server</div>\n        </div>\n      );\n    },\n    Source: ({ id }) => {\n      const info = useStack(id)?.info;\n      return <StandardSource info={info} />;\n    },\n    // Branch: ({ id }) => {\n    //   const config = useFullStack(id)?.config;\n    //   const file_contents = config?.file_contents;\n    //   if (file_contents || !config?.branch) return null;\n    //   return (\n    //     <div className=\"flex items-center gap-2\">\n    //       <GitBranch className=\"w-4 h-4\" />\n    //       {config.branch}\n    //     </div>\n    //   );\n    // },\n    Services: ({ id }) => {\n      const info = useStack(id)?.info;\n      return (\n        <div className=\"flex gap-1\">\n          <div className=\"font-bold\">{info?.services.length}</div>\n          <div>Service{(info?.services.length ?? 0 > 1) ? \"s\" : \"\"}</div>\n        </div>\n      );\n    },\n  },\n\n  Status: {\n    NoConfig: ({ id }) => {\n      const config = useFullStack(id)?.config;\n      if (\n        !config ||\n        config?.files_on_host ||\n        config?.file_contents ||\n        config?.linked_repo ||\n        config?.repo\n      ) {\n        return null;\n      }\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card className=\"px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer\">\n              <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                Config Missing\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent>\n            <div className=\"grid gap-2\">\n              No configuration provided for stack. Cannot get stack state.\n              Either paste the compose file contents into the UI, or configure a\n              git repo containing your files.\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n    ProjectMissing: ({ id }) => {\n      const info = useStack(id)?.info;\n      const state = info?.state ?? Types.StackState.Unknown;\n      if (\n        !info ||\n        !info?.project_missing ||\n        [Types.StackState.Down, Types.StackState.Unknown].includes(state)\n      ) {\n        return null;\n      }\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card className=\"px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer\">\n              <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                Project Missing\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent>\n            <div className=\"grid gap-2\">\n              The compose project is not on the host. If the compose stack is\n              running, the 'Project Name' needs to be set. This can be found\n              with 'docker compose ls'.\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n    RemoteErrors: ({ id }) => {\n      const info = useFullStack(id)?.info;\n      const errors = info?.remote_errors;\n      if (!info || !errors || errors.length === 0) {\n        return null;\n      }\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card className=\"px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer\">\n              <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                Remote Error\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent>\n            <div>\n              There are errors reading the remote file contents. See{\" \"}\n              <span className=\"font-bold\">Info</span> tab for details.\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n    UpdateAvailable: ({ id }) => <UpdateAvailable id={id} />,\n    Hash: ({ id }) => {\n      const info = useStack(id)?.info;\n      const fullInfo = useFullStack(id)?.info;\n      const state = info?.state;\n      const stackDown =\n        state === undefined ||\n        state === Types.StackState.Unknown ||\n        state === Types.StackState.Down;\n      if (\n        stackDown ||\n        info?.project_missing ||\n        !info?.latest_hash ||\n        !fullInfo\n      ) {\n        return null;\n      }\n      const out_of_date =\n        info.deployed_hash && info.deployed_hash !== info.latest_hash;\n      return (\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <Card\n              className={cn(\n                \"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer\",\n                out_of_date && border_color_class_by_intention(\"Warning\")\n              )}\n            >\n              <div className=\"text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n                {info.deployed_hash ? \"deployed\" : \"latest\"}:{\" \"}\n                {info.deployed_hash || info.latest_hash}\n              </div>\n            </Card>\n          </TooltipTrigger>\n          <TooltipContent>\n            <div className=\"grid gap-2\">\n              <Badge\n                variant=\"secondary\"\n                className=\"w-fit text-muted-foreground\"\n              >\n                message\n              </Badge>\n              {fullInfo.deployed_message || fullInfo.latest_message}\n              {out_of_date && (\n                <>\n                  <Badge\n                    variant=\"secondary\"\n                    className={cn(\n                      \"w-fit text-muted-foreground border-[1px]\",\n                      border_color_class_by_intention(\"Warning\")\n                    )}\n                  >\n                    latest\n                  </Badge>\n                  <div>\n                    <span className=\"text-muted-foreground\">\n                      {info.latest_hash}\n                    </span>\n                    : {fullInfo.latest_message}\n                  </div>\n                </>\n              )}\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      );\n    },\n    Refresh: ({ id }) => {\n      const { toast } = useToast();\n      const inv = useInvalidate();\n      const { mutate, isPending } = useWrite(\"RefreshStackCache\", {\n        onSuccess: () => {\n          inv([\"ListStacks\"], [\"GetStack\", { stack: id }]);\n          toast({ title: \"Refreshed stack status cache\" });\n        },\n      });\n      return (\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          onClick={() => {\n            mutate({ stack: id });\n            toast({ title: \"Triggered refresh of stack status cache\" });\n          }}\n        >\n          {isPending ? (\n            <Loader2 className=\"w-4 h-4 animate-spin\" />\n          ) : (\n            <RefreshCcw className=\"w-4 h-4\" />\n          )}\n        </Button>\n      );\n    },\n  },\n\n  Actions: {\n    DeployStack,\n    PullStack,\n    RestartStack,\n    PauseUnpauseStack,\n    StartStopStack,\n    DestroyStack,\n  },\n\n  Page: {},\n\n  Config: ConfigInfoServicesLog,\n\n  DangerZone: ({ id }) => <DeleteResource type=\"Stack\" id={id} />,\n\n  ResourcePageHeader: ({ id }) => {\n    const stack = useStack(id);\n    return (\n      <ResourcePageHeader\n        intent={stack_state_intention(stack?.info.state)}\n        icon={<StackIcon id={id} size={8} />}\n        type=\"Stack\"\n        id={id}\n        resource={stack}\n        state={stack?.info.state}\n        status={\n          stack?.info.state === Types.StackState.Unhealthy\n            ? stack?.info.status\n            : undefined\n        }\n      />\n    );\n  },\n};\n\nexport const UpdateAvailable = ({\n  id,\n  small = false,\n}: {\n  id: string;\n  small?: boolean;\n}) => {\n  const info = useStack(id)?.info;\n  const state = info?.state ?? Types.StackState.Unknown;\n  if (\n    !info ||\n    !!info?.services.every((service) => !service.update_available) ||\n    [Types.StackState.Down, Types.StackState.Unknown].includes(state)\n  ) {\n    return null;\n  }\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <div\n          className={cn(\n            \"px-2 py-1 border rounded-md border-blue-400 hover:border-blue-500 opacity-50 hover:opacity-70 transition-colors cursor-pointer flex items-center gap-2\",\n            small ? \"px-2 py-1\" : \"px-3 py-2\"\n          )}\n        >\n          <CircleArrowUp className=\"w-4 h-4\" />\n          {!small && (\n            <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis\">\n              Update\n              {(info?.services.filter((s) => s.update_available).length ?? 0) >\n              1\n                ? \"s\"\n                : \"\"}{\" \"}\n              Available\n            </div>\n          )}\n        </div>\n      </TooltipTrigger>\n      <TooltipContent className=\"flex flex-col gap-2 w-fit\">\n        {info?.services\n          .filter((service) => service.update_available)\n          .map((s) => (\n            <div className=\"text-sm flex gap-2\">\n              <div className=\"text-muted-foreground\">{s.service}</div>\n              <div className=\"text-muted-foreground\"> - </div>\n              <div>{s.image}</div>\n            </div>\n          ))}\n      </TooltipContent>\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/stack/info.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { ReactNode, useState } from \"react\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@ui/card\";\nimport { useFullStack, useStack } from \".\";\nimport { cn, updateLogToHtml } from \"@lib/utils\";\nimport { language_from_path, MonacoEditor } from \"@components/monaco\";\nimport { usePermissions } from \"@lib/hooks\";\nimport { ConfirmUpdate } from \"@components/config/util\";\nimport { useLocalStorage, useWrite } from \"@lib/hooks\";\nimport { Button } from \"@ui/button\";\nimport { FilePlus, History } from \"lucide-react\";\nimport { useToast } from \"@ui/use-toast\";\nimport { ConfirmButton, ShowHideButton, CopyButton } from \"@components/util\";\nimport { DEFAULT_STACK_FILE_CONTENTS } from \"./config\";\nimport { Types } from \"komodo_client\";\n\nexport const StackInfo = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const [edits, setEdits] = useLocalStorage<Record<string, string | undefined>>(\n    `stack-${id}-edits`,\n    {}\n  );\n  const [show, setShow] = useState<Record<string, boolean | undefined>>({});\n  const { canWrite } = usePermissions({ type: \"Stack\", id });\n  const { toast } = useToast();\n  const { mutateAsync, isPending } = useWrite(\"WriteStackFileContents\", {\n    onSuccess: (res) => {\n      toast({\n        title: res.success ? \"Contents written.\" : \"Failed to write contents.\",\n        variant: res.success ? undefined : \"destructive\",\n      });\n    },\n  });\n\n  const not_down = useStack(id)?.info.state !== Types.StackState.Down;\n  const stack = useFullStack(id);\n  // const state = useStack(id)?.info.state ?? Types.StackState.Unknown;\n  // const is_down = [Types.StackState.Down, Types.StackState.Unknown].includes(\n  //   state\n  // );\n\n  const file_on_host = stack?.config?.files_on_host ?? false;\n  const git_repo = !!(stack?.config?.repo || stack?.config?.linked_repo);\n  const canEdit = canWrite && (file_on_host || git_repo);\n  const editFileCallback = (path: string) => (contents: string) =>\n    setEdits({ ...edits, [path]: contents });\n\n  // Collect deployed / latest contents, joining\n  // them by path.\n  // Only unmatched latest contents end up in latest_contents.\n  // const deployed_contents: {\n  //   path: string;\n  //   deployed: string;\n  //   modified: string | undefined;\n  // }[] = [];\n\n  // if (!is_down) {\n  //   for (const content of stack?.info?.deployed_contents ?? []) {\n  //     const latest = stack?.info?.remote_contents?.find(\n  //       (latest) => latest.path === content.path\n  //     );\n  //     const modified =\n  //       latest?.contents &&\n  //       (latest.contents !== content.contents ? latest.contents : undefined);\n  //     deployed_contents.push({\n  //       path: content.path,\n  //       deployed: content.contents,\n  //       modified,\n  //     });\n  //   }\n  // }\n\n  const latest_contents = stack?.info?.remote_contents;\n  const latest_errors = stack?.info?.remote_errors;\n\n  // Contents will be default hidden if there is more than 2 file editor to show\n  const default_show_contents = !latest_contents || latest_contents.length < 3;\n\n  return (\n    <Section titleOther={titleOther}>\n      {/* Errors */}\n      {latest_errors &&\n        latest_errors.length > 0 &&\n        latest_errors.map((error) => (\n          <Card key={error.path} className=\"flex flex-col gap-4\">\n            <CardHeader className=\"flex flex-row justify-between items-center pb-0\">\n              <div className=\"font-mono flex gap-2\">\n                <div className=\"text-muted-foreground\">Path:</div>\n                {error.path}\n              </div>\n              {canEdit && (\n                <ConfirmButton\n                  title=\"Initialize File\"\n                  icon={<FilePlus className=\"w-4 h-4\" />}\n                  onClick={() => {\n                    if (stack) {\n                      mutateAsync({\n                        stack: stack.name,\n                        file_path: error.path,\n                        contents: DEFAULT_STACK_FILE_CONTENTS,\n                      });\n                    }\n                  }}\n                  loading={isPending}\n                />\n              )}\n            </CardHeader>\n            <CardContent className=\"pr-8\">\n              <pre\n                dangerouslySetInnerHTML={{\n                  __html: updateLogToHtml(error.contents),\n                }}\n                className=\"max-h-[500px] overflow-y-auto\"\n              />\n            </CardContent>\n          </Card>\n        ))}\n\n      {/* Update deployed contents with diff */}\n      {/* {!is_down && deployed_contents.length > 0 && (\n        <Card>\n          <CardHeader className=\"flex flex-col gap-2\">\n            deployed contents:{\" \"}\n          </CardHeader>\n          <CardContent>\n            {deployed_contents.map((content) => {\n              return (\n                <pre key={content.path} className=\"flex flex-col gap-2\">\n                  <div className=\"flex justify-between items-center\">\n                    <div>path: {content.path}</div>\n                    {canEdit && (\n                      <div className=\"flex items-center gap-2\">\n                        <Button\n                          variant=\"outline\"\n                          onClick={() =>\n                            setEdits({ ...edits, [content.path]: undefined })\n                          }\n                          className=\"flex items-center gap-2\"\n                          disabled={!edits[content.path]}\n                        >\n                          <History className=\"w-4 h-4\" />\n                          Reset\n                        </Button>\n                        <ConfirmUpdate\n                          previous={{\n                            contents: content.modified ?? content.deployed,\n                          }}\n                          content={{ contents: edits[content.path] }}\n                          onConfirm={() => {\n                            if (stack) {\n                              mutateAsync({\n                                stack: stack.name,\n                                file_path: content.path,\n                                contents: edits[content.path]!,\n                              }).then(() =>\n                                setEdits({\n                                  ...edits,\n                                  [content.path]: undefined,\n                                })\n                              );\n                            }\n                          }}\n                          disabled={!edits[content.path]}\n                        />\n                      </div>\n                    )}\n                  </div>\n                  {content.modified ? (\n                    <MonacoDiffEditor\n                      original={\"# Deployed contents\\n\" + content.deployed}\n                      modified={edits[content.path] ?? content.modified}\n                      language=\"yaml\"\n                      readOnly={!canEdit}\n                      hideUnchangedRegions={false}\n                      onModifiedValueChange={editFileCallback(content.path)}\n                    />\n                  ) : (\n                    <MonacoEditor\n                      value={edits[content.path] ?? content.deployed}\n                      language=\"yaml\"\n                      readOnly={!canEdit}\n                      onValueChange={editFileCallback(content.path)}\n                    />\n                  )}\n                </pre>\n              );\n            })}\n          </CardContent>\n        </Card>\n      )} */}\n\n      {/* Update latest contents */}\n      {latest_contents &&\n        latest_contents.length > 0 &&\n        latest_contents.map((content) => {\n          const showContents = show[content.path] ?? default_show_contents;\n          const handleToggleShow = () => {\n            setShow((show) => ({\n              ...show,\n              [content.path]: !(show[content.path] ?? default_show_contents),\n            }));\n          };\n          return (\n            <Card key={content.path} className=\"flex flex-col gap-4\">\n              <CardHeader\n                className={cn(\n                  \"flex flex-row justify-between items-center group cursor-pointer\",\n                  showContents && \"pb-0\"\n                )}\n                onClick={handleToggleShow}\n                tabIndex={0}\n                role=\"button\"\n                aria-pressed={showContents}\n                onKeyDown={(e) => {\n                  if (\n                    (e.key === \"Enter\" || e.key === \" \") &&\n                    e.target === e.currentTarget\n                  ) {\n                    if (e.key === \" \") e.preventDefault();\n                    handleToggleShow();\n                  }\n                }}\n              >\n                <CardTitle className=\"font-mono flex gap-2 items-center\">\n                  <div className=\"flex gap-2 items-center\">\n                    <span className=\"text-muted-foreground\">File:</span>\n                    <span>{content.path}</span>\n                    <span onClick={(e) => e.stopPropagation()} data-copy-button>\n                      <CopyButton content={content.path} label=\"file path\" />\n                    </span>\n                  </div>\n                </CardTitle>\n                <div className=\"flex items-center gap-2\">\n                  {canEdit && (\n                    <>\n                      <Button\n                        variant=\"outline\"\n                        onClick={(e) => {\n                          e.stopPropagation();\n                          setEdits({ ...edits, [content.path]: undefined });\n                        }}\n                        className=\"flex items-center gap-2\"\n                        disabled={!edits[content.path]}\n                      >\n                        <History className=\"w-4 h-4\" />\n                        Reset\n                      </Button>\n                      <span onClick={(e) => e.stopPropagation()}>\n                        <ConfirmUpdate\n                          previous={{ contents: content.contents }}\n                          content={{ contents: edits[content.path] }}\n                          onConfirm={async () => {\n                            if (stack) {\n                              return await mutateAsync({\n                                stack: stack.name,\n                                file_path: content.path,\n                                contents: edits[content.path]!,\n                              }).then(() =>\n                                setEdits({\n                                  ...edits,\n                                  [content.path]: undefined,\n                                })\n                              );\n                            }\n                          }}\n                          disabled={!edits[content.path]}\n                          language=\"yaml\"\n                          loading={isPending}\n                        />\n                      </span>\n                    </>\n                  )}\n                  <ShowHideButton\n                    show={showContents}\n                    setShow={() => {}}\n                  />\n                </div>\n              </CardHeader>\n              {showContents && (\n                <CardContent className=\"pr-8\">\n                  <MonacoEditor\n                    value={edits[content.path] ?? content.contents}\n                    language={language_from_path(content.path)}\n                    readOnly={!canEdit}\n                    filename={content.path}\n                    onValueChange={editFileCallback(content.path)}\n                  />\n                </CardContent>\n              )}\n            </Card>\n          );\n        })}\n\n      {stack?.info?.deployed_config && not_down && (\n        <Card className=\"flex flex-col gap-4\">\n          <CardHeader className=\"pb-0\">\n            <CardTitle className=\"font-mono\">Deployed config:</CardTitle>\n            <CardDescription>\n              Output of '<code>docker compose config</code>' when Stack was last\n              deployed.\n            </CardDescription>\n          </CardHeader>\n\n          <CardContent className=\"pr-8\">\n            <MonacoEditor\n              value={stack.info.deployed_config}\n              language=\"yaml\"\n              readOnly\n            />\n          </CardContent>\n        </Card>\n      )}\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/stack/log.tsx",
    "content": "import { LocalStorageSetter, useLocalStorage, useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { ReactNode } from \"react\";\nimport { useStack } from \".\";\nimport { Log, LogSection } from \"@components/log\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n} from \"@ui/dropdown-menu\";\nimport { CaretSortIcon } from \"@radix-ui/react-icons\";\n\nexport const StackLogs = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const stackInfo = useStack(id)?.info;\n  const [selectedServices, setServices] = useLocalStorage<string[]>(\n    `stack-${id}-log-services`,\n    []\n  );\n  if (\n    stackInfo === undefined ||\n    stackInfo.state === Types.StackState.Unknown ||\n    stackInfo.state === Types.StackState.Down\n  ) {\n    return null;\n  }\n  return (\n    <StackLogsInner\n      id={id}\n      titleOther={titleOther}\n      services={stackInfo.services.map((s) => ({\n        service: s.service,\n        selected: selectedServices.includes(s.service),\n      }))}\n      setServices={setServices}\n    />\n  );\n};\n\nconst StackLogsInner = ({\n  id,\n  titleOther,\n  services,\n  setServices,\n}: {\n  id: string;\n  titleOther: ReactNode;\n  services: Array<{ service: string; selected: boolean }>;\n  setServices: (state: string[] | LocalStorageSetter<string[]>) => void;\n}) => {\n  const selected = services.filter((s) => s.selected);\n  return (\n    <LogSection\n      regular_logs={(timestamps, stream, tail, poll) =>\n        NoSearchLogs(\n          id,\n          services.filter((s) => s.selected).map((s) => s.service),\n          tail,\n          timestamps,\n          stream,\n          poll\n        )\n      }\n      search_logs={(timestamps, terms, invert, poll) =>\n        SearchLogs(\n          id,\n          services.filter((s) => s.selected).map((s) => s.service),\n          terms,\n          invert,\n          timestamps,\n          poll\n        )\n      }\n      titleOther={titleOther}\n      extraParams={\n        <DropdownMenu>\n          <DropdownMenuTrigger>\n            <div className=\"px-3 py-2 border rounded-md flex items-center gap-2 hover:bg-accent/70 text-sm\">\n              <div className=\"text-muted-foreground\">Services:</div>{\" \"}\n              {selected.length === 0\n                ? \"All\"\n                : selected.map((s) => s.service).join(\", \")}\n              <CaretSortIcon className=\"h-4 w-4 opacity-50\" />\n            </div>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"start\">\n            {services.map((s) => {\n              return (\n                <DropdownMenuCheckboxItem\n                  key={s.service}\n                  checked={s.selected}\n                  onClick={(e) => {\n                    e.preventDefault();\n                    if (s.selected) {\n                      setServices((services) =>\n                        services.filter((service) => service !== s.service)\n                      );\n                    } else {\n                      setServices((services) => [...services, s.service]);\n                    }\n                  }}\n                >\n                  {s.service}\n                </DropdownMenuCheckboxItem>\n              );\n            })}\n          </DropdownMenuContent>\n        </DropdownMenu>\n      }\n    />\n  );\n};\n\nconst NoSearchLogs = (\n  id: string,\n  services: string[],\n  tail: number,\n  timestamps: boolean,\n  stream: string,\n  poll: boolean\n) => {\n  const { data: log, refetch } = useRead(\n    \"GetStackLog\",\n    {\n      stack: id,\n      services,\n      tail,\n      timestamps,\n    },\n    { refetchInterval: poll ? 3000 : false }\n  );\n  return {\n    Log: (\n      <div className=\"relative\">\n        <Log log={log} stream={stream as \"stdout\" | \"stderr\"} />\n      </div>\n    ),\n    refetch,\n    stderr: !!log?.stderr,\n  };\n};\n\nconst SearchLogs = (\n  id: string,\n  services: string[],\n  terms: string[],\n  invert: boolean,\n  timestamps: boolean,\n  poll: boolean\n) => {\n  const { data: log, refetch } = useRead(\n    \"SearchStackLog\",\n    {\n      stack: id,\n      services,\n      terms,\n      combinator: Types.SearchCombinator.And,\n      invert,\n      timestamps,\n    },\n    { refetchInterval: poll ? 10000 : false }\n  );\n  return {\n    Log: (\n      <div className=\"h-full relative\">\n        <Log log={log} stream=\"stdout\" />\n      </div>\n    ),\n    refetch,\n    stderr: !!log?.stderr,\n  };\n};\n"
  },
  {
    "path": "frontend/src/components/resources/stack/services.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport {\n  container_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\nimport { useRead } from \"@lib/hooks\";\nimport { cn } from \"@lib/utils\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { useStack } from \".\";\nimport { Types } from \"komodo_client\";\nimport { Fragment, ReactNode } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { Button } from \"@ui/button\";\nimport { Layers2 } from \"lucide-react\";\nimport {\n  ContainerPortsTableView,\n  DockerResourceLink,\n  StatusBadge,\n} from \"@components/util\";\n\nexport const StackServices = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther: ReactNode;\n}) => {\n  const info = useStack(id)?.info;\n  const server_id = info?.server_id;\n  const state = info?.state ?? Types.StackState.Unknown;\n  const services = useRead(\n    \"ListStackServices\",\n    { stack: id },\n    { refetchInterval: 10_000 }\n  ).data;\n  if (\n    !services ||\n    services.length === 0 ||\n    [Types.StackState.Unknown, Types.StackState.Down].includes(state)\n  ) {\n    return null;\n  }\n  return (\n    <Section titleOther={titleOther}>\n      <div className=\"lg:min-h-[300px]\">\n        <DataTable\n          tableKey=\"StackServices\"\n          data={services}\n          columns={[\n            {\n              accessorKey: \"service\",\n              size: 200,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Service\" />\n              ),\n              cell: ({ row }) => {\n                const state = row.original.container?.state;\n                const color = stroke_color_class_by_intention(\n                  container_state_intention(state)\n                );\n                return (\n                  <Link\n                    to={`/stacks/${id}/service/${row.original.service}`}\n                    onClick={(e) => e.stopPropagation()}\n                  >\n                    <Button\n                      variant=\"link\"\n                      className=\"flex gap-2 items-center p-0\"\n                    >\n                      <Layers2 className={cn(\"w-4 h-4\", color)} />\n                      {row.original.service}\n                    </Button>\n                  </Link>\n                );\n              },\n            },\n            {\n              accessorKey: \"container.state\",\n              size: 160,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"State\" />\n              ),\n              cell: ({ row }) => {\n                const state = row.original.container?.state;\n                return (\n                  <StatusBadge\n                    text={state}\n                    intent={container_state_intention(state)}\n                  />\n                );\n              },\n            },\n            {\n              accessorKey: \"container.image\",\n              size: 300,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Image\" />\n              ),\n              cell: ({ row }) =>\n                server_id && (\n                  <DockerResourceLink\n                    type=\"image\"\n                    server_id={server_id}\n                    name={row.original.container?.image}\n                    id={row.original.container?.image_id}\n                  />\n                ),\n              // size: 200,\n            },\n            {\n              accessorKey: \"container.networks.0\",\n              size: 200,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Networks\" />\n              ),\n              cell: ({ row }) =>\n                (row.original.container?.networks?.length ?? 0) > 0 ? (\n                  <div className=\"flex items-center gap-2 flex-wrap\">\n                    {server_id &&\n                      row.original.container?.networks?.map((network, i) => (\n                        <Fragment key={network}>\n                          <DockerResourceLink\n                            type=\"network\"\n                            server_id={server_id}\n                            name={network}\n                          />\n                          {i !==\n                            row.original.container!.networks!.length - 1 && (\n                            <div className=\"text-muted-foreground\">|</div>\n                          )}\n                        </Fragment>\n                      ))}\n                  </div>\n                ) : (\n                  server_id &&\n                  row.original.container?.network_mode && (\n                    <DockerResourceLink\n                      type=\"network\"\n                      server_id={server_id}\n                      name={row.original.container.network_mode}\n                    />\n                  )\n                ),\n            },\n            {\n              accessorKey: \"container.ports.0\",\n              size: 200,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Ports\" />\n              ),\n              cell: ({ row }) => (\n                <ContainerPortsTableView\n                  ports={row.original.container?.ports ?? []}\n                  server_id={server_id}\n                />\n              ),\n            },\n          ]}\n        />\n      </div>\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/resources/stack/table.tsx",
    "content": "import { useRead, useSelectedResources } from \"@lib/hooks\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { ResourceLink, StandardSource } from \"../common\";\nimport { TableTags } from \"@components/tags\";\nimport { StackComponents, UpdateAvailable } from \".\";\nimport { Types } from \"komodo_client\";\nimport { useCallback } from \"react\";\n\nexport const StackTable = ({ stacks }: { stacks: Types.StackListItem[] }) => {\n  const servers = useRead(\"ListServers\", {}).data;\n  const serverName = useCallback(\n    (id: string) => servers?.find((server) => server.id === id)?.name,\n    [servers]\n  );\n\n  const [_, setSelectedResources] = useSelectedResources(\"Stack\");\n\n  return (\n    <DataTable\n      tableKey=\"Stacks\"\n      data={stacks}\n      selectOptions={{\n        selectKey: ({ name }) => name,\n        onSelect: setSelectedResources,\n      }}\n      columns={[\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Name\" />\n          ),\n          accessorKey: \"name\",\n          cell: ({ row }) => {\n            return (\n              <div className=\"flex items-center justify-between gap-2\">\n                <ResourceLink type=\"Stack\" id={row.original.id} />\n                <UpdateAvailable id={row.original.id} small />\n              </div>\n            );\n          },\n          size: 200,\n        },\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Source\" />\n          ),\n          accessorKey: \"info.repo\",\n          cell: ({ row }) => <StandardSource info={row.original.info} />,\n          size: 200,\n        },\n        {\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"Server\" />\n          ),\n          accessorKey: \"info.server_id\",\n          sortingFn: (a, b) => {\n            const sa = serverName(a.original.info.server_id);\n            const sb = serverName(b.original.info.server_id);\n\n            if (!sa && !sb) return 0;\n            if (!sa) return 1;\n            if (!sb) return -1;\n\n            if (sa > sb) return 1;\n            else if (sa < sb) return -1;\n            else return 0;\n          },\n          cell: ({ row }) => (\n            <ResourceLink type=\"Server\" id={row.original.info.server_id} />\n          ),\n          size: 200,\n        },\n        {\n          accessorKey: \"info.state\",\n          header: ({ column }) => (\n            <SortableHeader column={column} title=\"State\" />\n          ),\n          cell: ({ row }) => <StackComponents.State id={row.original.id} />,\n          size: 120,\n        },\n        {\n          header: \"Tags\",\n          cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/sidebar.tsx",
    "content": "import { SIDEBAR_RESOURCES, cn, usableResourcePath } from \"@lib/utils\";\nimport { Button } from \"@ui/button\";\nimport {\n  AlertTriangle,\n  Bell,\n  Box,\n  Boxes,\n  CalendarDays,\n  LayoutDashboard,\n  Settings,\n} from \"lucide-react\";\nimport { Link, useLocation } from \"react-router-dom\";\nimport { ResourceComponents } from \"./resources\";\nimport { Separator } from \"@ui/separator\";\nimport { ReactNode } from \"react\";\nimport { useAtom } from \"jotai\";\nimport { homeViewAtom } from \"@main\";\n\nexport const Sidebar = () => {\n  const [view, setView] = useAtom(homeViewAtom);\n  return (\n    <div className=\"fixed top-0 pt-[84px] w-[200px] border-r hidden lg:block pr-8 pb-8 h-screen overflow-y-auto\">\n      <div className=\"flex flex-col gap-1\">\n        <SidebarLink\n          label=\"Dashboard\"\n          to=\"/\"\n          icon={<LayoutDashboard className=\"w-4 h-4\" />}\n          onClick={() => setView(\"Dashboard\")}\n          highlighted={view === \"Dashboard\"}\n        />\n        <SidebarLink\n          label=\"Resources\"\n          to=\"/\"\n          icon={<Boxes className=\"w-4 h-4\" />}\n          onClick={() => setView(\"Resources\")}\n          highlighted={view === \"Resources\"}\n        />\n        <SidebarLink\n          label=\"Containers\"\n          to=\"/containers\"\n          icon={<Box className=\"w-4 h-4\" />}\n        />\n\n        <Separator className=\"my-3\" />\n\n        <p className=\"pl-4 pb-1 text-xs text-muted-foreground\">Resources</p>\n        {SIDEBAR_RESOURCES.map((type) => {\n          const RTIcon = ResourceComponents[type].Icon;\n          const name = type === \"ResourceSync\" ? \"Sync\" : type;\n          return (\n            <SidebarLink\n              key={type}\n              label={`${name}s`}\n              to={`/${usableResourcePath(type)}`}\n              icon={<RTIcon />}\n            />\n          );\n        })}\n\n        <Separator className=\"my-3\" />\n\n        <p className=\"pl-4 pb-1 text-xs text-muted-foreground\">Notifications</p>\n        <SidebarLink\n          label=\"Alerts\"\n          to=\"/alerts\"\n          icon={<AlertTriangle className=\"w-4 h-4\" />}\n        />\n        <SidebarLink\n          label=\"Updates\"\n          to=\"/updates\"\n          icon={<Bell className=\"w-4 h-4\" />}\n        />\n        \n        <Separator className=\"my-3\" />\n\n        <SidebarLink\n          label=\"Schedules\"\n          to=\"/schedules\"\n          icon={<CalendarDays className=\"w-4 h-4\" />}\n        />\n\n        <SidebarLink\n          label=\"Settings\"\n          to=\"/settings\"\n          icon={<Settings className=\"w-4 h-4\" />}\n        />\n        {/* <Separator className=\"mt-3\" /> */}\n      </div>\n    </div>\n  );\n};\n\nconst SidebarLink = ({\n  to,\n  icon,\n  label,\n  onClick,\n  highlighted,\n}: {\n  to: string;\n  icon: ReactNode;\n  label: string;\n  onClick?: () => void;\n  highlighted?: boolean;\n}) => {\n  const location = useLocation();\n  const hl =\n    \"/\" + location.pathname.split(\"/\")[1] === to && (highlighted ?? true);\n  return (\n    <Link to={to} className=\"w-full ml-[2px]\" onClick={onClick}>\n      <Button\n        variant=\"link\"\n        className={cn(\n          \"flex justify-start items-center gap-2 w-full hover:bg-accent/75\",\n          hl && \"bg-accent/75\"\n        )}\n        tabIndex={-1}\n      >\n        {icon}\n        {label}\n      </Button>\n    </Link>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/tags/index.tsx",
    "content": "import {\n  useTags,\n  useInvalidate,\n  useRead,\n  useShiftKeyListener,\n  useWrite,\n} from \"@lib/hooks\";\nimport { cn, filterBySplit } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { Badge } from \"@ui/badge\";\nimport { Button } from \"@ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport { useToast } from \"@ui/use-toast\";\nimport { MinusCircle, PlusCircle, SearchX, Tag, X } from \"lucide-react\";\nimport { ReactNode, useEffect, useState } from \"react\";\nimport { tag_background_class } from \"@lib/color\";\n\ntype TargetExcludingSystem = Exclude<Types.ResourceTarget, { type: \"System\" }>;\n\nexport const TagsFilter = () => {\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const { tags, add_tag, remove_tag, clear_tags } = useTags();\n  const all_tags = useRead(\"ListTags\", {}).data;\n  const filtered = filterBySplit(all_tags, search, (item) => item.name);\n  useShiftKeyListener(\"T\", () => setOpen(true));\n  useShiftKeyListener(\"C\", () => clear_tags());\n  return (\n    <div className=\"flex gap-3 items-center\">\n      {tags.length > 0 && (\n        <Button\n          variant=\"destructive\"\n          className=\"px-2 py-1.5 h-fit\"\n          onClick={() => clear_tags()}\n        >\n          <X className=\"w-4 h-4\" />\n        </Button>\n      )}\n\n      <TagsFilterTags tag_ids={tags} onBadgeClick={remove_tag} />\n\n      <Popover\n        open={open}\n        onOpenChange={(open) => {\n          setSearch(\"\");\n          setOpen(open);\n        }}\n      >\n        <PopoverTrigger asChild>\n          <Button variant=\"outline\" className=\"flex items-center gap-2\">\n            <Tag className=\"w-3 h-3\" />\n            Tag Filter\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent\n          className=\"w-[200px] max-h-[200px] p-0\"\n          sideOffset={12}\n          align=\"end\"\n        >\n          <Command shouldFilter={false}>\n            <CommandInput\n              placeholder=\"Search Tags\"\n              className=\"h-9\"\n              value={search}\n              onValueChange={setSearch}\n            />\n            <CommandList>\n              <CommandEmpty className=\"flex justify-evenly items-center pt-2\">\n                No Tags Found\n                <SearchX className=\"w-3 h-3\" />\n              </CommandEmpty>\n\n              <CommandGroup>\n                {filtered\n                  ?.filter((tag) => !tags.includes(tag._id!.$oid))\n                  .map((tag) => (\n                    <CommandItem\n                      key={tag.name}\n                      onSelect={() => {\n                        add_tag(tag._id!.$oid);\n                        setSearch(\"\");\n                        setOpen(false);\n                      }}\n                      className=\"flex items-center justify-between cursor-pointer\"\n                    >\n                      <div className=\"p-1\">{tag.name}</div>\n                      <div\n                        className={cn(\n                          \"w-[25px] h-[25px] rounded-sm bg-opacity-70\",\n                          tag_background_class(tag.color)\n                        )}\n                      />\n                    </CommandItem>\n                  ))}\n              </CommandGroup>\n            </CommandList>\n          </Command>\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n};\n\nexport const TagsFilterTags = ({\n  tag_ids,\n  onBadgeClick,\n}: {\n  tag_ids?: string[];\n  onBadgeClick?: (tag_id: string) => void;\n}) => {\n  const all_tags = useRead(\"ListTags\", {}).data;\n  const get_tag = (tag_id: string) =>\n    all_tags?.find((t) => t._id?.$oid === tag_id);\n  return (\n    <>\n      {tag_ids?.map((tag_id) => {\n        const tag = get_tag(tag_id);\n        const color = tag_background_class(tag?.color);\n        return (\n          <Badge\n            key={tag_id}\n            variant=\"secondary\"\n            className={cn(\n              \"flex gap-1 px-2 py-1.5 cursor-pointer text-nowrap bg-opacity-30 hover:bg-opacity-70\",\n              color,\n              `hover:${color}`\n            )}\n            onClick={() => onBadgeClick && onBadgeClick(tag_id)}\n          >\n            {tag?.name ?? \"unknown\"}\n            <MinusCircle className=\"w-3 h-3\" />\n          </Badge>\n        );\n      })}\n    </>\n  );\n};\n\nexport const ResourceTags = ({\n  target,\n  click_to_delete,\n  className,\n  disabled,\n}: {\n  target: TargetExcludingSystem;\n  click_to_delete?: boolean;\n  className?: string;\n  disabled?: boolean;\n}) => {\n  const { toast } = useToast();\n  const inv = useInvalidate();\n  const { type, id } = target;\n  const resource = useRead(`List${type}s`, {}).data?.find((d) => d.id === id);\n  const { mutate } = useWrite(\"UpdateResourceMeta\", {\n    onSuccess: () => {\n      inv([`List${type}s`]);\n      toast({ title: \"Removed tag\" });\n    },\n  });\n\n  return (\n    <TagsWithBadge\n      tag_ids={resource?.tags}\n      onBadgeClick={(tag_id) => {\n        if (!click_to_delete) return;\n        if (disabled) return;\n        mutate({\n          target,\n          tags: resource!.tags.filter((tag) => tag !== tag_id),\n        });\n      }}\n      className={className}\n      icon={!disabled && click_to_delete && <MinusCircle className=\"w-3 h-3\" />}\n    />\n  );\n};\n\nexport const TagsWithBadge = ({\n  tag_ids,\n  onBadgeClick,\n  className,\n  icon,\n}: {\n  tag_ids?: string[];\n  onBadgeClick?: (tag_id: string) => void;\n  className?: string;\n  icon?: ReactNode;\n}) => {\n  const all_tags = useRead(\"ListTags\", {}).data;\n  const get_tag = (tag_id: string) =>\n    all_tags?.find((t) => t._id?.$oid === tag_id);\n  return (\n    <>\n      {tag_ids?.map((tag_id) => {\n        const tag = get_tag(tag_id);\n        const color = tag_background_class(tag?.color);\n        return (\n          <Badge\n            key={tag_id}\n            variant=\"secondary\"\n            className={cn(\n              \"gap-2 px-1.5 py-0.5 cursor-pointer text-nowrap bg-opacity-30 hover:bg-opacity-70\",\n              color,\n              `hover:${color}`,\n              className\n            )}\n            onClick={() => onBadgeClick && onBadgeClick(tag_id)}\n          >\n            {tag?.name ?? \"unknown\"}\n            {icon}\n          </Badge>\n        );\n      })}\n    </>\n  );\n};\n\nexport const TableTags = ({ tag_ids }: { tag_ids: string[] }) => {\n  const { toggle_tag } = useTags();\n  return (\n    <div className=\"flex gap-1 flex-wrap\">\n      <TagsWithBadge tag_ids={tag_ids} onBadgeClick={toggle_tag} />\n    </div>\n  );\n};\n\nexport const AddTags = ({ target }: { target: TargetExcludingSystem }) => {\n  const { toast } = useToast();\n\n  const { type, id } = target;\n  const resource = useRead(`List${type}s`, {}).data?.find((d) => d.id === id);\n\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n\n  useShiftKeyListener(\"T\", () => setOpen(true));\n\n  const all_tags = useRead(\"ListTags\", {}).data ?? [];\n  const all_tag_names = all_tags.map((tag) => tag.name);\n\n  const inv = useInvalidate();\n\n  const { mutate: update } = useWrite(\"UpdateResourceMeta\", {\n    onSuccess: () => {\n      inv([`List${type}s`]);\n      toast({ title: `Added tag ${search}` });\n      setOpen(false);\n    },\n  });\n\n  const { mutateAsync: create } = useWrite(\"CreateTag\", {\n    onSuccess: () => inv([`ListTags`]),\n  });\n\n  useEffect(() => {\n    if (open) setSearch(\"\");\n  }, [open]);\n\n  const create_tag = async () => {\n    if (!search) return toast({ title: \"Must provide tag name in input\" });\n    const tag = await create({ name: search });\n    update({\n      target,\n      tags: [...(resource?.tags ?? []), tag._id!.$oid],\n    });\n    setOpen(false);\n  };\n\n  if (!resource) return null;\n\n  const filtered = filterBySplit(all_tags, search, (item) => item.name)?.sort(\n    (a, b) => {\n      if (a.name > b.name) {\n        return 1;\n      } else if (a.name < b.name) {\n        return -1;\n      } else {\n        return 0;\n      }\n    }\n  );\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button variant=\"secondary\" className=\"px-2 py-0.5 h-fit\">\n          <PlusCircle className=\"w-3\" />\n          {/* <Badge\n            variant=\"outline\"\n            className=\"text-muted-foreground hidden md:inline-flex\"\n          >\n            shift + t\n          </Badge> */}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[200px] p-0\" sideOffset={12} align=\"start\">\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Search / Create\"\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandEmpty className=\"m-1\">\n              <Button\n                variant=\"ghost\"\n                onClick={create_tag}\n                className=\"w-full flex items-center justify-between hover:bg-accent\"\n              >\n                Create Tag\n                <PlusCircle className=\"w-4\" />\n              </Button>\n            </CommandEmpty>\n            <CommandGroup>\n              {filtered\n                ?.filter((tag) => !resource?.tags.includes(tag._id!.$oid))\n                .map((tag) => (\n                  <CommandItem\n                    key={tag._id?.$oid}\n                    value={tag.name}\n                    onSelect={() =>\n                      update({\n                        target,\n                        tags: [...(resource?.tags ?? []), tag._id!.$oid],\n                      })\n                    }\n                    className=\"cursor-pointer flex items-center justify-between gap-2\"\n                  >\n                    <div className=\"p-1\">{tag.name}</div>\n                    <div\n                      className={cn(\n                        \"w-[25px] h-[25px] rounded-sm\",\n                        tag_background_class(tag.color)\n                      )}\n                    />\n                  </CommandItem>\n                ))}\n              {search && !all_tag_names.includes(search) && (\n                <CommandItem onSelect={create_tag} className=\"cursor-pointer\">\n                  <div className=\"w-full p-1 flex items-center justify-between\">\n                    Create Tag\n                    <PlusCircle className=\"w-4\" />\n                  </div>\n                </CommandItem>\n              )}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/terminal/container.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { komodo_client, useLocalStorage } from \"@lib/hooks\";\nimport { Button } from \"@ui/button\";\nimport { CardTitle } from \"@ui/card\";\nimport { Input } from \"@ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { RefreshCcw } from \"lucide-react\";\nimport { ReactNode, useCallback, useState } from \"react\";\nimport { Terminal } from \".\";\nimport { ConnectExecQuery, TerminalCallbacks } from \"komodo_client\";\n\nconst BASE_SHELLS = [\"sh\", \"bash\"];\n\nexport const ContainerTerminal = ({\n  query: { type, query },\n  titleOther,\n}: {\n  query: ConnectExecQuery;\n  titleOther?: ReactNode;\n}) => {\n  const [_reconnect, _setReconnect] = useState(false);\n  const triggerReconnect = () => _setReconnect((r) => !r);\n  const [_clear, _setClear] = useState(false);\n\n  const storageKey =\n    type === \"container\"\n      ? `server-${query.server}-${query.container}-shell-v1`\n      : type === \"deployment\"\n        ? `deployment-${query.deployment}-shell-v1`\n        : `stack-${query.stack}-${query.service}-shell-v1`;\n\n  const [shell, setShell] = useLocalStorage(storageKey, \"sh\");\n  const [otherShell, setOtherShell] = useState(\"\");\n\n  const make_ws = useCallback(\n    (callbacks: TerminalCallbacks) =>\n      komodo_client().connect_exec({\n        query: { type, query: { ...query, shell } } as any,\n        ...callbacks,\n      }),\n    [query, shell]\n  );\n\n  return (\n    <Section\n      titleOther={titleOther}\n      actions={\n        <div className=\"flex items-center gap-4 mr-[16px]\">\n          <CardTitle className=\"text-muted-foreground flex items-center gap-2\">\n            docker exec -it container\n            <Select value={shell} onValueChange={setShell}>\n              <SelectTrigger className=\"w-[120px]\">\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectGroup>\n                  {[\n                    ...BASE_SHELLS,\n                    ...(!BASE_SHELLS.includes(shell) ? [shell] : []),\n                  ].map((shell) => (\n                    <SelectItem key={shell} value={shell}>\n                      {shell}\n                    </SelectItem>\n                  ))}\n                  <Input\n                    placeholder=\"other\"\n                    value={otherShell}\n                    onChange={(e) => setOtherShell(e.target.value)}\n                    onKeyDown={(e) => {\n                      if (e.key === \"Enter\") {\n                        setShell(otherShell);\n                        setOtherShell(\"\");\n                      } else {\n                        e.stopPropagation();\n                      }\n                    }}\n                  />\n                </SelectGroup>\n              </SelectContent>\n            </Select>\n          </CardTitle>\n          <Button\n            className=\"flex items-center gap-2\"\n            variant=\"secondary\"\n            onClick={() => triggerReconnect()}\n          >\n            Reconnect\n            <RefreshCcw className=\"w-4 h-4\" />\n          </Button>\n        </div>\n      }\n    >\n      <div className=\"min-h-[65vh]\">\n        <Terminal\n          make_ws={make_ws}\n          selected={true}\n          _clear={_clear}\n          _reconnect={_reconnect}\n        />\n      </div>\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/terminal/index.tsx",
    "content": "import { cn } from \"@lib/utils\";\nimport { useTheme } from \"@ui/theme\";\nimport { FitAddon } from \"@xterm/addon-fit\";\nimport { ITheme } from \"@xterm/xterm\";\nimport { TerminalCallbacks } from \"komodo_client\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { useXTerm, UseXTermProps } from \"react-xtermjs\";\n\nconst LIGHT_THEME: ITheme = {\n  background: \"#f7f8f9\",\n  foreground: \"#24292e\",\n  cursor: \"#24292e\",\n  selectionBackground: \"#c8d9fa\",\n};\n\nconst DARK_THEME: ITheme = {\n  background: \"#151b25\",\n  foreground: \"#f6f8fa\",\n  cursor: \"#ffffff\",\n  selectionBackground: \"#6e778a\",\n};\n\nexport const Terminal = ({\n  make_ws,\n  selected,\n  _reconnect,\n  _clear,\n}: {\n  make_ws: (callbacks: TerminalCallbacks) => WebSocket;\n  selected: boolean;\n  _reconnect: boolean;\n  _clear?: boolean;\n}) => {\n  const { currentTheme } = useTheme();\n  const theme = currentTheme === \"dark\" ? DARK_THEME : LIGHT_THEME;\n  const wsRef = useRef<WebSocket | null>(null);\n  const fitRef = useRef<FitAddon>(new FitAddon());\n\n  const resize = () => {\n    fitRef.current.fit();\n    if (term) {\n      if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {\n        const json = JSON.stringify({\n          rows: term.rows,\n          cols: term.cols,\n        });\n        const buf = new Uint8Array(json.length + 1);\n        buf[0] = 0xff; // resize prefix\n        for (let i = 0; i < json.length; i++) buf[i + 1] = json.charCodeAt(i);\n        wsRef.current.send(buf);\n      }\n      term.focus();\n    }\n  };\n\n  const onStdin = (data: string) => {\n    // This is data user writes to stdin\n    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;\n\n    const buf = new Uint8Array(data.length + 1);\n    buf[0] = 0x00; // data prefix\n    for (let i = 0; i < data.length; i++) buf[i + 1] = data.charCodeAt(i);\n    wsRef.current.send(buf);\n  };\n\n  useEffect(resize, [selected]);\n\n  const params: UseXTermProps = useMemo(\n    () => ({\n      options: {\n        convertEol: false,\n        cursorBlink: true,\n        cursorStyle: \"block\",\n        fontFamily: \"monospace\",\n        scrollback: 5000,\n        // This is handled in ws on_message handler\n        scrollOnUserInput: false,\n        theme,\n      },\n      listeners: {\n        onResize: resize,\n        onData: onStdin,\n      },\n      addons: [fitRef.current],\n    }),\n    [theme]\n  );\n\n  const { instance: term, ref: termRef } = useXTerm(params);\n\n  const viewport = (term as any)?._core?.viewport?._viewportElement as\n    | HTMLDivElement\n    | undefined;\n\n  useEffect(() => {\n    if (!term || !viewport) return;\n\n    let delta = 0;\n    term.attachCustomWheelEventHandler((e) => {\n      e.preventDefault();\n      // This is used to make touchpad and mousewheel more similar\n      delta += Math.sign(e.deltaY) * Math.sqrt(Math.abs(e.deltaY)) * 20;\n      return false;\n    });\n    const int = setInterval(() => {\n      if (Math.abs(delta) < 1) return;\n      viewport.scrollTop += delta;\n      delta = 0;\n    }, 100);\n    return () => clearInterval(int);\n  }, [term, termRef.current]);\n\n  useEffect(() => {\n    if (!selected || !term) return;\n\n    term.clear();\n\n    let debounce = -1;\n\n    const callbacks: TerminalCallbacks = {\n      on_login: () => {\n        // console.log(\"logged in terminal\");\n      },\n      on_open: resize,\n      on_message: (e: MessageEvent<any>) => {\n        term.write(new Uint8Array(e.data as ArrayBuffer), () => {\n          if (viewport) {\n            viewport.scrollTop = viewport.scrollHeight - viewport.clientHeight;\n          }\n          clearTimeout(debounce);\n          debounce = setTimeout(() => {\n            if (!viewport) return;\n            viewport.scrollTop = viewport.scrollHeight - viewport.clientHeight;\n          }, 500);\n        });\n      },\n      on_close: () => {\n        term.writeln(\"\\r\\n\\x1b[33m[connection closed]\\x1b[0m\");\n      },\n    };\n\n    const ws = make_ws(callbacks);\n\n    wsRef.current = ws;\n\n    return () => {\n      ws.close();\n      wsRef.current = null;\n    };\n  }, [term, viewport, make_ws, selected, _reconnect]);\n\n  useEffect(() => term?.clear(), [_clear]);\n\n  return (\n    <div\n      ref={termRef}\n      className={cn(\"w-full h-[65vh]\", selected ? \"\" : \"hidden\")}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/terminal/server.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { ReactNode, useCallback, useState } from \"react\";\nimport { komodo_client, useLocalStorage, useRead, useWrite } from \"@lib/hooks\";\nimport { Card, CardContent, CardHeader } from \"@ui/card\";\nimport { Badge } from \"@ui/badge\";\nimport { Button } from \"@ui/button\";\nimport { Loader2, Plus, RefreshCcw, X } from \"lucide-react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport {\n  Command,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport { filterBySplit } from \"@lib/utils\";\nimport { useServer } from \"@components/resources/server\";\nimport { Terminal } from \".\";\nimport { TerminalCallbacks } from \"komodo_client\";\n\nexport const ServerTerminals = ({\n  id,\n  titleOther,\n}: {\n  id: string;\n  titleOther?: ReactNode;\n}) => {\n  const { data: terminals, refetch: refetchTerminals } = useRead(\n    \"ListTerminals\",\n    {\n      server: id,\n      fresh: true,\n    },\n    {\n      refetchInterval: 5000,\n    }\n  );\n  const { mutateAsync: create_terminal, isPending: create_pending } =\n    useWrite(\"CreateTerminal\");\n  const { mutateAsync: delete_terminal } = useWrite(\"DeleteTerminal\");\n  const [_selected, setSelected] = useLocalStorage<{\n    selected: string | undefined;\n  }>(`server-${id}-selected-terminal-v1`, { selected: undefined });\n  const terminals_disabled = useServer(id)?.info.terminals_disabled ?? true;\n\n  const selected = _selected.selected ?? terminals?.[0]?.name;\n\n  const [_reconnect, _setReconnect] = useState(false);\n  const triggerReconnect = () => _setReconnect((r) => !r);\n\n  const create = async (command: string) => {\n    if (!terminals || terminals_disabled) return;\n    const name = next_terminal_name(\n      command,\n      terminals.map((t) => t.name)\n    );\n    await create_terminal({\n      server: id,\n      name,\n      command,\n    });\n    refetchTerminals();\n    setTimeout(() => {\n      setSelected({\n        selected: name,\n      });\n    }, 100);\n  };\n\n  return (\n    <Section titleOther={titleOther}>\n      <Card>\n        <CardHeader className=\"flex flex-row gap-4 items-center justify-between flex-wrap\">\n          <div className=\"flex gap-4 items-center flex-wrap\">\n            {terminals?.map(({ name: terminal, stored_size_kb }) => (\n              <Badge\n                key={terminal}\n                variant={terminal === selected ? \"default\" : \"secondary\"}\n                className=\"w-fit min-w-[150px] px-2 py-1 cursor-pointer flex gap-4 justify-between\"\n                onClick={() => setSelected({ selected: terminal })}\n              >\n                <div className=\"text-sm w-full flex gap-1 items-center justify-between\">\n                  {terminal}\n                  {/* <div className=\"min-w-[20px] max-w-[70px] text-xs text-muted-foreground text-nowrap whitespace-nowrap overflow-hidden overflow-ellipsis\">\n                    {command}\n                  </div> */}\n                  <div className=\"text-muted-foreground text-xs\">\n                    {stored_size_kb.toFixed()} KiB\n                  </div>\n                </div>\n                <Button\n                  className=\"p-1 h-fit\"\n                  variant=\"destructive\"\n                  onClick={async (e) => {\n                    e.stopPropagation();\n                    await delete_terminal({ server: id, terminal });\n                    refetchTerminals();\n                    if (selected === terminal) {\n                      setSelected({ selected: undefined });\n                    }\n                  }}\n                >\n                  <X className=\"w-4 h-4\" />\n                </Button>\n              </Badge>\n            ))}\n            {terminals && !terminals_disabled && (\n              <NewTerminal create={create} pending={create_pending} />\n            )}\n          </div>\n          <Button\n            className=\"flex items-center gap-2\"\n            variant=\"secondary\"\n            onClick={() => triggerReconnect()}\n          >\n            Reconnect\n            <RefreshCcw className=\"w-4 h-4\" />\n          </Button>\n        </CardHeader>\n        <CardContent className=\"min-h-[65vh]\">\n          {terminals?.map(({ name: terminal }) => (\n            <ServerTerminal\n              key={terminal}\n              server={id}\n              terminal={terminal}\n              selected={selected === terminal}\n              _reconnect={_reconnect}\n            />\n          ))}\n        </CardContent>\n      </Card>\n    </Section>\n  );\n};\n\nconst ServerTerminal = ({\n  server,\n  terminal,\n  selected,\n  _reconnect,\n}: {\n  server: string;\n  terminal: string;\n  selected: boolean;\n  _reconnect: boolean;\n}) => {\n  const make_ws = useCallback(\n    (callbacks: TerminalCallbacks) =>\n      komodo_client().connect_terminal({\n        query: { server, terminal },\n        ...callbacks,\n      }),\n    [server, terminal]\n  );\n  return (\n    <Terminal make_ws={make_ws} selected={selected} _reconnect={_reconnect} />\n  );\n};\n\nconst BASE_SHELLS = [\"bash\", \"sh\"];\n\nconst NewTerminal = ({\n  create,\n  pending,\n}: {\n  create: (shell: string) => Promise<void>;\n  pending: boolean;\n}) => {\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const [shells, setShells] = useLocalStorage(\"server-shells-v1\", BASE_SHELLS);\n  const filtered = filterBySplit(shells, search, (item) => item);\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          className=\"flex items-center gap-2\"\n          disabled={pending}\n        >\n          New Terminal\n          {pending ? (\n            <Loader2 className=\"w-4 h-4 animate-spin\" />\n          ) : (\n            <Plus className=\"w-4 h-4\" />\n          )}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[200px] max-h-[300px] p-0\" align=\"start\">\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Enter shell\"\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandGroup>\n              {filtered.map((shell) => (\n                <CommandItem\n                  key={shell}\n                  onSelect={() => {\n                    create(shell);\n                    setOpen(false);\n                  }}\n                  className=\"flex items-center justify-between cursor-pointer\"\n                >\n                  <div className=\"p-1\">{shell}</div>\n                  {!BASE_SHELLS.includes(shell) && (\n                    <Button\n                      variant=\"destructive\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        setShells((shells) =>\n                          shells.filter((s) => s !== shell)\n                        );\n                      }}\n                      className=\"p-1 h-fit\"\n                    >\n                      <X className=\"w-4 h-4\" />\n                    </Button>\n                  )}\n                </CommandItem>\n              ))}\n              {filtered.length === 0 && (\n                <CommandItem\n                  onSelect={() => {\n                    setShells((shells) => [...shells, search]);\n                    create(search);\n                    setOpen(false);\n                  }}\n                  className=\"flex items-center justify-between cursor-pointer\"\n                >\n                  <div className=\"p-1\">{search}</div>\n                  <Plus className=\"w-4 h-4\" />\n                </CommandItem>\n              )}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nconst next_terminal_name = (command: string, terminal_names: string[]) => {\n  const shell = command.split(\" \")[0];\n  for (let i = 1; i <= terminal_names.length + 1; i++) {\n    const name = i > 1 ? `${shell} ${i}` : shell;\n    if (!terminal_names.includes(name)) {\n      return name;\n    }\n  }\n  return shell;\n};\n"
  },
  {
    "path": "frontend/src/components/topbar/components.tsx",
    "content": "import {\n  LOGIN_TOKENS,\n  useManageUser,\n  useRead,\n  useResourceParamType,\n  useUser,\n  useUserInvalidate,\n} from \"@lib/hooks\";\nimport { ResourceComponents } from \"../resources\";\nimport {\n  AlertTriangle,\n  ArrowLeftRight,\n  Bell,\n  Box,\n  Boxes,\n  Calendar,\n  CalendarDays,\n  Check,\n  Circle,\n  FileQuestion,\n  FolderTree,\n  Keyboard,\n  LayoutDashboard,\n  Loader2,\n  LogOut,\n  Plus,\n  Settings,\n  User,\n  Users,\n  X,\n} from \"lucide-react\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@ui/dropdown-menu\";\nimport { Button } from \"@ui/button\";\nimport { Link } from \"react-router-dom\";\nimport {\n  cn,\n  RESOURCE_TARGETS,\n  usableResourcePath,\n  version_is_none,\n} from \"@lib/utils\";\nimport { useAtom } from \"jotai\";\nimport { ReactNode, useState } from \"react\";\nimport { HomeView, homeViewAtom } from \"@main\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { Badge } from \"@ui/badge\";\nimport { ConfirmButton } from \"../util\";\nimport { Types } from \"komodo_client\";\nimport { UpdateDetails, UpdateUser } from \"@components/updates/details\";\nimport { fmt_date, fmt_operation, fmt_version } from \"@lib/formatting\";\nimport { ResourceLink, ResourceNameSimple } from \"@components/resources/common\";\nimport { UsableResource } from \"@types\";\nimport { AlertLevel } from \"@components/alert\";\nimport { AlertDetailsDialogContent } from \"@components/alert/details\";\nimport { Separator } from \"@ui/separator\";\n\nexport const MobileDropdown = () => {\n  const type = useResourceParamType();\n  const Components = type && ResourceComponents[type];\n  const [view, setView] = useAtom<HomeView>(homeViewAtom);\n\n  const [icon, title] = Components\n    ? [<Components.Icon />, (type === \"ResourceSync\" ? \"Sync\" : type) + \"s\"]\n    : location.pathname === \"/\" && view === \"Dashboard\"\n      ? [<LayoutDashboard className=\"w-4 h-4\" />, \"Dashboard\"]\n      : location.pathname === \"/\" && view === \"Resources\"\n        ? [<Boxes className=\"w-4 h-4\" />, \"Resources\"]\n        : location.pathname === \"/\" && view === \"Tree\"\n          ? [<FolderTree className=\"w-4 h-4\" />, \"Tree\"]\n          : location.pathname === \"/containers\"\n            ? [<Box className=\"w-4 h-4\" />, \"Containers\"]\n            : location.pathname === \"/settings\"\n              ? [<Settings className=\"w-4 h-4\" />, \"Settings\"]\n              : location.pathname === \"/schedules\"\n                ? [<CalendarDays className=\"w-4 h-4\" />, \"Schedules\"]\n                : location.pathname === \"/alerts\"\n                  ? [<AlertTriangle className=\"w-4 h-4\" />, \"Alerts\"]\n                  : location.pathname === \"/updates\"\n                    ? [<Bell className=\"w-4 h-4\" />, \"Updates\"]\n                    : location.pathname.split(\"/\")[1] === \"user-groups\"\n                      ? [<Users className=\"w-4 h-4\" />, \"User Groups\"]\n                      : location.pathname.split(\"/\")[1] === \"users\"\n                        ? [<User className=\"w-4 h-4\" />, \"Users\"]\n                        : [<FileQuestion className=\"w-4 h-4\" />, \"Unknown\"];\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild className=\"lg:hidden justify-self-end\">\n        <Button\n          variant=\"ghost\"\n          className=\"flex justify-start items-center gap-2 w-36 px-3\"\n        >\n          {icon}\n          {title}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent className=\"w-36\" side=\"bottom\" align=\"start\">\n        <DropdownMenuGroup>\n          <DropdownLinkItem\n            label=\"Dashboard\"\n            icon={<LayoutDashboard className=\"w-4 h-4\" />}\n            to=\"/\"\n            onClick={() => setView(\"Dashboard\")}\n          />\n          <DropdownLinkItem\n            label=\"Resources\"\n            icon={<Boxes className=\"w-4 h-4\" />}\n            to=\"/\"\n            onClick={() => setView(\"Resources\")}\n          />\n          <DropdownLinkItem\n            label=\"Containers\"\n            icon={<Box className=\"w-4 h-4\" />}\n            to=\"/containers\"\n          />\n\n          <DropdownMenuSeparator />\n\n          {RESOURCE_TARGETS.map((type) => {\n            const RTIcon = ResourceComponents[type].Icon;\n            const name = type === \"ResourceSync\" ? \"Sync\" : type;\n            return (\n              <DropdownLinkItem\n                key={type}\n                label={`${name}s`}\n                icon={<RTIcon />}\n                to={`/${usableResourcePath(type)}`}\n              />\n            );\n          })}\n\n          <DropdownMenuSeparator />\n\n          <DropdownLinkItem\n            label=\"Alerts\"\n            icon={<AlertTriangle className=\"w-4 h-4\" />}\n            to=\"/alerts\"\n          />\n\n          <DropdownLinkItem\n            label=\"Updates\"\n            icon={<Bell className=\"w-4 h-4\" />}\n            to=\"/updates\"\n          />\n\n          <DropdownMenuSeparator />\n\n          <DropdownLinkItem\n            label=\"Schedules\"\n            icon={<CalendarDays className=\"w-4 h-4\" />}\n            to=\"/schedules\"\n          />\n\n          <DropdownLinkItem\n            label=\"Settings\"\n            icon={<Settings className=\"w-4 h-4\" />}\n            to=\"/settings\"\n          />\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nconst DropdownLinkItem = ({\n  label,\n  icon,\n  to,\n  onClick,\n}: {\n  label: string;\n  icon: ReactNode;\n  to: string;\n  onClick?: () => void;\n}) => {\n  return (\n    <Link to={to} onClick={onClick}>\n      <DropdownMenuItem className=\"flex items-center gap-2 cursor-pointer\">\n        {icon}\n        {label}\n      </DropdownMenuItem>\n    </Link>\n  );\n};\n\nexport const UserDropdown = () => {\n  const [_, setRerender] = useState(false);\n  const rerender = () => setRerender((r) => !r);\n  const [viewLogout, setViewLogout] = useState(false);\n  const [open, _setOpen] = useState(false);\n  const setOpen = (open: boolean) => {\n    _setOpen(open);\n    if (open) {\n      setViewLogout(false);\n    }\n  };\n  const user = useUser().data;\n  const userInvalidate = useUserInvalidate();\n  const accounts = LOGIN_TOKENS.accounts();\n  return (\n    <DropdownMenu open={open} onOpenChange={setOpen}>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" className=\"flex items-center gap-2 px-2\">\n          <UsernameView\n            username={user?.username}\n            avatar={(user?.config.data as any).avatar}\n          />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent\n        className=\"w-[260px] flex flex-col gap-2 items-end p-2\"\n        side=\"bottom\"\n        align=\"end\"\n        sideOffset={16}\n      >\n        <div className=\"flex items-center justify-between gap-2 w-full\">\n          <div className=\"flex gap-2 items-center text-muted-foreground pl-4 text-sm\">\n            <ArrowLeftRight className=\"w-4\" />\n            Switch accounts\n          </div>\n          <Button\n            className=\"px-2 py-0\"\n            variant={viewLogout ? \"secondary\" : \"outline\"}\n            onClick={() => setViewLogout((l) => !l)}\n          >\n            <Settings className=\"w-4\" />\n          </Button>\n        </div>\n\n        {accounts.map((login) => (\n          <Account\n            login={login}\n            current_id={user?._id?.$oid}\n            setOpen={setOpen}\n            rerender={rerender}\n            viewLogout={viewLogout}\n          />\n        ))}\n\n        <Separator />\n\n        <Link\n          to={`/login?${new URLSearchParams({ backto: `${location.pathname}${location.search}` })}`}\n          className=\"w-full\"\n        >\n          <Button\n            variant=\"ghost\"\n            onClick={() => setOpen(false)}\n            className=\"flex gap-1 items-center justify-center w-full\"\n          >\n            Add account\n            <Plus className=\"w-4\" />\n          </Button>\n        </Link>\n\n        {viewLogout && (\n          <ConfirmButton\n            title=\"Log Out All\"\n            icon={<LogOut className=\"w-4 h-4\" />}\n            variant=\"destructive\"\n            className=\"flex gap-2 items-center justify-center w-full max-w-full\"\n            onClick={() => {\n              LOGIN_TOKENS.remove_all();\n              userInvalidate();\n            }}\n          />\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nconst Account = ({\n  login,\n  current_id,\n  setOpen,\n  rerender,\n  viewLogout,\n}: {\n  login: Types.JwtResponse;\n  current_id?: string;\n  setOpen: (open: boolean) => void;\n  rerender: () => void;\n  viewLogout: boolean;\n}) => {\n  const res = useRead(\"GetUsername\", { user_id: login.user_id });\n  if (!res.data) return;\n  const selected = login.user_id === current_id;\n  return (\n    <div className=\"flex gap-2 items-center w-full\">\n      <Button\n        variant={selected ? \"secondary\" : \"ghost\"}\n        className=\"flex gap-2 items-center justify-between w-full\"\n        onClick={() => {\n          if (selected) {\n            // Noop\n            setOpen(false);\n            return;\n          }\n          LOGIN_TOKENS.change(login.user_id);\n          location.reload();\n        }}\n      >\n        <div className=\"flex items-center gap-2\">\n          <UsernameView\n            username={res.data?.username}\n            avatar={res.data?.avatar}\n          />\n        </div>\n        {selected && (\n          <Circle className=\"w-3 h-3 stroke-none transition-colors fill-green-500\" />\n        )}\n      </Button>\n\n      {viewLogout && (\n        <Button\n          variant=\"destructive\"\n          className=\"px-2 py-0\"\n          onClick={() => {\n            LOGIN_TOKENS.remove(login.user_id);\n            if (selected) {\n              location.reload();\n            } else {\n              rerender();\n            }\n          }}\n        >\n          <LogOut className=\"w-4\" />\n        </Button>\n      )}\n    </div>\n  );\n};\n\nconst UsernameView = ({\n  username,\n  avatar,\n  full,\n}: {\n  username: string | undefined;\n  avatar: string | undefined;\n  full?: boolean;\n}) => {\n  return (\n    <>\n      {avatar ? <img src={avatar} className=\"w-4\" /> : <User className=\"w-4\" />}\n      <div\n        className={cn(\n          \"overflow-hidden overflow-ellipsis\",\n          full ? \"max-w-[200px]\" : \"hidden xl:flex max-w-[140px]\"\n        )}\n      >\n        {username}\n      </div>\n    </>\n  );\n};\n\nexport const TopbarUpdates = () => {\n  const updates = useRead(\"ListUpdates\", {}).data;\n\n  const last_opened = useUser().data?.last_update_view;\n  const unseen_update = updates?.updates.some(\n    (u) => u.start_ts > (last_opened ?? Number.MAX_SAFE_INTEGER)\n  );\n\n  const userInvalidate = useUserInvalidate();\n  const { mutate } = useManageUser(\"SetLastSeenUpdate\", {\n    onSuccess: userInvalidate,\n  });\n\n  return (\n    <DropdownMenu onOpenChange={(o) => o && mutate({})}>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\" className=\"relative\">\n          <Bell className=\"w-4 h-4\" />\n          <Circle\n            className={cn(\n              \"absolute top-2 right-2 w-2 h-2 stroke-blue-500 fill-blue-500 transition-opacity\",\n              unseen_update ? \"opacity-1\" : \"opacity-0\"\n            )}\n          />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent\n        className=\"w-[100vw] md:w-[500px] h-[500px] overflow-auto\"\n        sideOffset={20}\n      >\n        <DropdownMenuGroup>\n          {updates?.updates.map((update) => (\n            <SingleUpdate update={update} key={update.id} />\n          ))}\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nconst SingleUpdate = ({ update }: { update: Types.UpdateListItem }) => {\n  const Components =\n    update.target.type !== \"System\"\n      ? ResourceComponents[update.target.type]\n      : null;\n\n  const Icon = () => {\n    if (update.status === Types.UpdateStatus.Complete) {\n      if (update.success) return <Check className=\"w-4 h-4 stroke-green-500\" />;\n      else return <X className=\"w-4 h-4 stroke-red-500\" />;\n    } else return <Loader2 className=\"w-4 h-4 animate-spin\" />;\n  };\n\n  return (\n    <UpdateDetails id={update.id}>\n      <div className=\"px-2 py-4 hover:bg-muted transition-colors border-b last:border-none cursor-pointer\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"text-sm w-full\">\n            <div className=\"flex items-center gap-2\">\n              <Icon />\n              {fmt_operation(update.operation)}\n              <div className=\"text-xs text-muted-foreground\">\n                {!version_is_none(update.version) &&\n                  fmt_version(update.version)}\n              </div>\n            </div>\n            <div className=\"flex items-center gap-2 text-muted-foreground\">\n              {Components && (\n                <>\n                  <Components.Icon />\n                  <ResourceNameSimple\n                    type={update.target.type as UsableResource}\n                    id={update.target.id}\n                  />\n                </>\n              )}\n              {!Components && (\n                <>\n                  <Settings className=\"w-4 h-4\" />\n                  System\n                </>\n              )}\n            </div>\n          </div>\n          <div className=\"text-xs text-muted-foreground w-48\">\n            <div className=\"flex items-center gap-2 h-[20px]\">\n              <Calendar className=\"w-4 h-4\" />\n              <div>\n                {update.status === Types.UpdateStatus.InProgress\n                  ? \"ongoing\"\n                  : fmt_date(new Date(update.start_ts))}\n              </div>\n            </div>\n            <UpdateUser user_id={update.operator} iconSize={4} defaultAvatar />\n          </div>\n        </div>\n      </div>\n    </UpdateDetails>\n  );\n};\n\nexport const TopbarAlerts = () => {\n  const { data } = useRead(\n    \"ListAlerts\",\n    { query: { resolved: false } },\n    { refetchInterval: 3_000 }\n  );\n  const [open, setOpen] = useState(false);\n\n  // If this is set, details will open.\n  const [alert, setAlert] = useState<Types.Alert>();\n\n  if (!data || data.alerts.length === 0) {\n    return null;\n  }\n\n  return (\n    <>\n      <DropdownMenu open={open} onOpenChange={setOpen}>\n        <DropdownMenuTrigger asChild disabled={!data?.alerts.length}>\n          <Button variant=\"ghost\" size=\"icon\" className=\"relative\">\n            <AlertTriangle className=\"w-4 h-4\" />\n            {!!data?.alerts.length && (\n              <div className=\"absolute top-0 right-0 w-4 h-4 bg-red-500 flex items-center justify-center text-[10px] text-white rounded-full\">\n                {data.alerts.length}\n              </div>\n            )}\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent sideOffset={20}>\n          {data?.alerts.map((alert) => (\n            <DropdownMenuItem\n              key={alert._id?.$oid}\n              className=\"flex items-center gap-8 border-b last:border-none cursor-pointer\"\n              onClick={() => setAlert(alert)}\n            >\n              <div className=\"w-24\">\n                <AlertLevel level={alert.level} />\n              </div>\n              <div className=\"w-64\">\n                <div className=\"w-fit\">\n                  <ResourceLink\n                    type={alert.target.type as UsableResource}\n                    id={alert.target.id}\n                    onClick={() => setOpen(false)}\n                  />\n                </div>\n              </div>\n              <p className=\"w-64\">{alert.data.type}</p>\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuContent>\n      </DropdownMenu>\n      <AlertDetails alert={alert} onClose={() => setAlert(undefined)} />\n    </>\n  );\n};\n\nconst AlertDetails = ({\n  alert,\n  onClose,\n}: {\n  alert: Types.Alert | undefined;\n  onClose: () => void;\n}) => (\n  <>\n    {alert && (\n      <Dialog open={!!alert} onOpenChange={(o) => !o && onClose()}>\n        <AlertDetailsDialogContent alert={alert} onClose={onClose} />\n      </Dialog>\n    )}\n  </>\n);\n\nexport const Docs = () => (\n  <a\n    href=\"https://komo.do/docs/intro\"\n    target=\"_blank\"\n    className=\"hidden lg:block\"\n  >\n    <Button variant=\"link\" size=\"sm\" className=\"px-2\">\n      <div>Docs</div>\n    </Button>\n  </a>\n);\n\nexport const Version = () => {\n  const version = useRead(\"GetVersion\", {}, { refetchInterval: 30_000 }).data\n    ?.version;\n\n  if (!version) return null;\n  return (\n    <a\n      href=\"https://github.com/moghtech/komodo/releases\"\n      target=\"_blank\"\n      className=\"hidden lg:block\"\n    >\n      <Button variant=\"link\" size=\"sm\" className=\"px-2\">\n        <div>v{version}</div>\n      </Button>\n    </a>\n  );\n};\n\nexport const KeyboardShortcuts = () => {\n  return (\n    <Dialog>\n      <DialogTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\" className=\"hidden md:flex\">\n          <Keyboard className=\"w-4 h-4\" />\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Keyboard Shortcuts</DialogTitle>\n        </DialogHeader>\n        <div className=\"grid gap-3 grid-cols-2 pt-8\">\n          <KeyboardShortcut label=\"Save\" keys={[\"Ctrl / Cmd\", \"Enter\"]} />\n          <KeyboardShortcut label=\"Go Home\" keys={[\"Shift\", \"H\"]} />\n\n          <KeyboardShortcut label=\"Go to Servers\" keys={[\"Shift\", \"G\"]} />\n          <KeyboardShortcut label=\"Go to Stacks\" keys={[\"Shift\", \"Z\"]} />\n          <KeyboardShortcut label=\"Go to Deployments\" keys={[\"Shift\", \"D\"]} />\n          <KeyboardShortcut label=\"Go to Builds\" keys={[\"Shift\", \"B\"]} />\n          <KeyboardShortcut label=\"Go to Repos\" keys={[\"Shift\", \"R\"]} />\n          <KeyboardShortcut label=\"Go to Procedures\" keys={[\"Shift\", \"P\"]} />\n\n          <KeyboardShortcut label=\"Search\" keys={[\"Shift\", \"S\"]} />\n          <KeyboardShortcut label=\"Add Filter Tag\" keys={[\"Shift\", \"T\"]} />\n          <KeyboardShortcut\n            label=\"Clear Filter Tags\"\n            keys={[\"Shift\", \"C\"]}\n            divider={false}\n          />\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst KeyboardShortcut = ({\n  label,\n  keys,\n  divider = true,\n}: {\n  label: string;\n  keys: string[];\n  divider?: boolean;\n}) => {\n  return (\n    <>\n      <div>{label}</div>\n      <div className=\"flex items-center gap-2\">\n        {keys.map((key) => (\n          <Badge variant=\"secondary\" key={key}>\n            {key}\n          </Badge>\n        ))}\n      </div>\n\n      {divider && (\n        <div className=\"col-span-full bg-gray-600 h-[1px] opacity-40\" />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/topbar/index.tsx",
    "content": "import { useShiftKeyListener } from \"@lib/hooks\";\nimport { Link } from \"react-router-dom\";\nimport { OmniSearch, OmniDialog } from \"../omnibar\";\nimport { WsStatusIndicator } from \"@lib/socket\";\nimport { ThemeToggle } from \"@ui/theme\";\nimport { useState } from \"react\";\nimport {\n  Docs,\n  KeyboardShortcuts,\n  MobileDropdown,\n  TopbarAlerts,\n  TopbarUpdates,\n  UserDropdown,\n  Version,\n} from \"./components\";\n\nexport const Topbar = () => {\n  const [omniOpen, setOmniOpen] = useState(false);\n  useShiftKeyListener(\"S\", () => setOmniOpen(true));\n\n  return (\n    <div className=\"fixed top-0 w-full bg-accent z-50 border-b shadow-sm\">\n      <div className=\"container h-16 flex items-center justify-between md:grid md:grid-cols-[auto_1fr] lg:grid-cols-3\">\n        {/* Logo */}\n        <Link\n          to=\"/\"\n          className=\"flex gap-3 items-center text-2xl tracking-widest md:mx-2\"\n        >\n          <img src=\"/komodo-512x512.png\" className=\"w-[32px]\" />\n          <div className=\"hidden lg:block\">KOMODO</div>\n        </Link>\n\n        {/* Searchbar */}\n        <div className=\"hidden lg:flex justify-center\">\n          <OmniSearch setOpen={setOmniOpen} />\n        </div>\n\n        {/* Shortcuts */}\n        <div className=\"flex justify-end items-center gap-1\">\n          <MobileDropdown />\n          <OmniSearch setOpen={setOmniOpen} className=\"lg:hidden\" />\n          <div className=\"flex gap-0\">\n            <Docs />\n            <Version />\n          </div>\n          <WsStatusIndicator />\n          <KeyboardShortcuts />\n          <TopbarAlerts />\n          <TopbarUpdates />\n          <ThemeToggle />\n          <UserDropdown />\n        </div>\n      </div>\n      <OmniDialog open={omniOpen} setOpen={setOmniOpen} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/updates/details.tsx",
    "content": "import {\n  Sheet,\n  SheetContent,\n  SheetDescription,\n  SheetHeader,\n  SheetTitle,\n  SheetTrigger,\n} from \"@ui/sheet\";\nimport {\n  Calendar,\n  Clock,\n  Link2,\n  Loader2,\n  Milestone,\n  Settings,\n  User,\n} from \"lucide-react\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@ui/card\";\nimport { ReactNode, useEffect, useState } from \"react\";\nimport { useRead } from \"@lib/hooks\";\nimport { ResourceComponents } from \"@components/resources\";\nimport { Link } from \"react-router-dom\";\nimport { fmt_duration, fmt_operation, fmt_version } from \"@lib/formatting\";\nimport {\n  cn,\n  updateLogToHtml,\n  usableResourcePath,\n  version_is_none,\n} from \"@lib/utils\";\nimport { UsableResource } from \"@types\";\nimport { CopyButton, UserAvatar } from \"@components/util\";\nimport { ResourceNameSimple } from \"@components/resources/common\";\nimport { useWebsocketMessages } from \"@lib/socket\";\nimport { MonacoDiffEditor } from \"@components/monaco\";\n\nexport const UpdateUser = ({\n  user_id,\n  className,\n  iconSize = 4,\n  defaultAvatar,\n  muted,\n}: {\n  user_id: string;\n  className?: string;\n  iconSize?: number;\n  defaultAvatar?: boolean;\n  muted?: boolean;\n}) => {\n  const res = useRead(\"GetUsername\", { user_id }).data;\n  const username = res?.username;\n  const avatar = res?.avatar;\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-2 text-nowrap\",\n        muted && \"text-muted-foreground\",\n        className\n      )}\n    >\n      {defaultAvatar ? (\n        <User className={`w-${iconSize} h-${iconSize}`} />\n      ) : (\n        <UserAvatar avatar={avatar} size={iconSize} />\n      )}\n      {username || \"Unknown\"}\n    </div>\n  );\n};\n\nexport const UpdateDetails = ({\n  id,\n  children,\n}: {\n  id: string;\n  children: ReactNode;\n}) => {\n  const [open, setOpen] = useState(false);\n  return (\n    <UpdateDetailsInner\n      id={id}\n      children={children}\n      open={open}\n      setOpen={setOpen}\n    />\n  );\n};\n\nexport const UpdateDetailsInner = ({\n  id,\n  children,\n  open,\n  setOpen,\n}: {\n  id: string;\n  open: boolean;\n  setOpen: React.Dispatch<React.SetStateAction<boolean>>;\n  children?: ReactNode;\n}) => {\n  return (\n    <Sheet open={open} onOpenChange={setOpen}>\n      <SheetTrigger asChild>{children}</SheetTrigger>\n      <SheetContent\n        className=\"mx-auto w-[1200px] max-w-[100vw] rounded-b-md\"\n        side=\"top\"\n      >\n        <UpdateDetailsContent id={id} open={open} setOpen={setOpen} />\n      </SheetContent>\n    </Sheet>\n  );\n};\n\nexport const UpdateDetailsContent = ({\n  id,\n  open,\n  setOpen,\n}: {\n  id: string;\n  open?: boolean;\n  setOpen: React.Dispatch<React.SetStateAction<boolean>>;\n}) => {\n  const { data: update, refetch } = useRead(\n    \"GetUpdate\",\n    { id },\n    { enabled: false }\n  );\n  useEffect(() => {\n    // handle open state change loading\n    if (open) {\n      refetch();\n    }\n  }, [open]);\n  // Since auto refetching is disabled, listen for updates on the update id and refetch\n  useWebsocketMessages(\"update-details\", (update) => {\n    if (update.id === id) refetch();\n  });\n  if (!update)\n    return (\n      <div className=\"w-full flex justify-center\">\n        <Loader2 className=\"w-8 h-8 animate-spin\" />\n      </div>\n    );\n  const Components =\n    update.target.type === \"System\"\n      ? null\n      : ResourceComponents[update.target.type];\n  return (\n    <>\n      <SheetHeader className=\"mb-4\">\n        <SheetTitle>\n          {fmt_operation(update.operation)}{\" \"}\n          {!version_is_none(update.version) && fmt_version(update.version)}\n        </SheetTitle>\n        <SheetDescription className=\"flex flex-col gap-2\">\n          <UpdateUser user_id={update.operator} />\n          <div className=\"flex gap-4\">\n            {Components ? (\n              <Link\n                to={`/${usableResourcePath(\n                  update.target.type as UsableResource\n                )}/${update.target.id}`}\n              >\n                <div\n                  className=\"flex items-center gap-2\"\n                  onClick={() => setOpen?.(false)}\n                >\n                  <Components.Icon id={update.target.id} />\n                  <ResourceNameSimple\n                    // will be UsableResource because Components exists in this branch\n                    type={update.target.type as UsableResource}\n                    id={update.target.id}\n                  />\n                </div>\n              </Link>\n            ) : (\n              <div className=\"flex items-center gap-2\">\n                <Settings className=\"w-4 h-4\" />\n                System\n              </div>\n            )}\n            {update.version && (\n              <div className=\"flex items-center gap-2\">\n                <Milestone className=\"w-4 h-4\" />\n                {fmt_version(update.version)}\n              </div>\n            )}\n          </div>\n          <div className=\"flex gap-4 items-center\">\n            <div className=\"flex items-center gap-2\">\n              <Calendar className=\"w-4 h-4\" />\n              {new Date(update.start_ts).toLocaleString()}\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <Clock className=\"w-4 h-4\" />\n              {update.end_ts\n                ? fmt_duration(update.start_ts, update.end_ts)\n                : \"ongoing\"}\n            </div>\n            <CopyButton\n              content={`${location.origin}/updates/${update._id?.$oid}`}\n              className=\"flex gap-3 items-center\"\n              icon={<Link2 className=\"w-4\" />}\n              label={\"shareable link\"}\n            />\n          </div>\n        </SheetDescription>\n      </SheetHeader>\n      <div className=\"grid gap-2 max-h-[calc(85vh-110px)] overflow-y-auto\">\n        {update.prev_toml && update.current_toml && (\n          <Card>\n            <CardHeader>\n              <CardTitle>Changes made</CardTitle>\n            </CardHeader>\n            <CardContent>\n              <MonacoDiffEditor\n                original={update.prev_toml}\n                modified={update.current_toml}\n                language=\"fancy_toml\"\n                readOnly\n              />\n            </CardContent>\n          </Card>\n        )}\n        {update.logs?.map((log, i) => (\n          <Card key={i}>\n            <CardHeader className=\"flex-col\">\n              <CardTitle>{log.stage}</CardTitle>\n              <CardDescription className=\"flex gap-2\">\n                <span>\n                  Stage {i + 1} of {update.logs.length}\n                </span>\n                <span>|</span>\n                <span className=\"flex items-center gap-2\">\n                  <Clock className=\"w-4 h-4\" />\n                  {fmt_duration(log.start_ts, log.end_ts)}\n                </span>\n              </CardDescription>\n            </CardHeader>\n            <CardContent className=\"flex flex-col gap-2\">\n              {log.command && (\n                <div>\n                  <CardDescription>command</CardDescription>\n                  <pre className=\"max-h-[500px] overflow-y-auto\">\n                    {log.command}\n                  </pre>\n                </div>\n              )}\n              {log.stdout && (\n                <div>\n                  <CardDescription>stdout</CardDescription>\n                  <pre\n                    dangerouslySetInnerHTML={{\n                      __html: updateLogToHtml(log.stdout),\n                    }}\n                    className=\"max-h-[500px] overflow-y-auto\"\n                  />\n                </div>\n              )}\n              {log.stderr && (\n                <div>\n                  <CardDescription>stderr</CardDescription>\n                  <pre\n                    dangerouslySetInnerHTML={{\n                      __html: updateLogToHtml(log.stderr),\n                    }}\n                    className=\"max-h-[500px] overflow-y-auto\"\n                  />\n                </div>\n              )}\n            </CardContent>\n          </Card>\n        ))}\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/updates/resource.tsx",
    "content": "import { useRead } from \"@lib/hooks\";\nimport { Button } from \"@ui/button\";\nimport {\n  Bell,\n  ExternalLink,\n  Calendar,\n  Check,\n  X,\n  Loader2,\n  Milestone,\n} from \"lucide-react\";\nimport { Link } from \"react-router-dom\";\nimport { Types } from \"komodo_client\";\nimport { Section } from \"@components/layouts\";\nimport { UpdateDetails, UpdateUser } from \"./details\";\nimport { UpdateStatus } from \"komodo_client/dist/types\";\nimport { fmt_date, fmt_operation, fmt_version } from \"@lib/formatting\";\nimport {\n  getUpdateQuery,\n  usableResourcePath,\n  version_is_none,\n} from \"@lib/utils\";\nimport { Card } from \"@ui/card\";\nimport { UsableResource } from \"@types\";\n\nconst UpdateCard = ({\n  update,\n  smallHidden,\n}: {\n  update: Types.UpdateListItem;\n  smallHidden?: boolean;\n}) => {\n  const Icon = () => {\n    if (update.status === UpdateStatus.Complete) {\n      if (update.success) return <Check className=\"w-4 stroke-green-500\" />;\n      else return <X className=\"w-4 stroke-red-500\" />;\n    } else return <Loader2 className=\"w-4 animate-spin\" />;\n  };\n\n  return (\n    <UpdateDetails id={update.id}>\n      <Card\n        className={`p-4 ${\n          smallHidden ? \"hidden xl:\" : \"\"\n        }flex justify-between cursor-pointer hover:bg-accent/50 transition-colors text-sm`}\n      >\n        <div className=\"grid gap-1 items-start\">\n          <div className=\"flex items-center gap-2\">\n            <Icon />\n            {fmt_operation(update.operation)}\n          </div>\n          {!version_is_none(update.version) && (\n            <div className=\"flex items-center gap-2 text-muted-foreground\">\n              <Milestone className=\"w-4\" />\n              {fmt_version(update.version)}\n            </div>\n          )}\n        </div>\n        <div className=\"grid gap-1\">\n          <div className=\"flex items-center gap-2 text-muted-foreground\">\n            <Calendar className=\"w-4\" />\n            {fmt_date(new Date(update.start_ts))}\n          </div>\n          <UpdateUser user_id={update.operator} muted />\n        </div>\n      </Card>\n    </UpdateDetails>\n  );\n};\n\nexport const AllUpdates = () => {\n  const updates = useRead(\"ListUpdates\", {}).data;\n\n  return (\n    <Section\n      title=\"Updates\"\n      icon={<Bell className=\"w-4 h-4\" />}\n      actions={\n        <Link to=\"/updates\">\n          <Button variant=\"secondary\" size=\"icon\">\n            <ExternalLink className=\"w-4 h-4\" />\n          </Button>\n        </Link>\n      }\n    >\n      <div className=\"grid md:grid-cols-2 xl:grid-cols-3 gap-4\">\n        {updates?.updates.slice(0, 3).map((update, i) => (\n          <UpdateCard update={update} key={update.id} smallHidden={i > 1} />\n        ))}\n      </div>\n    </Section>\n  );\n};\n\nexport const ResourceUpdates = ({ type, id }: Types.ResourceTarget) => {\n  const deployments = useRead(\"ListDeployments\", {}).data;\n\n  const updates = useRead(\"ListUpdates\", {\n    query: getUpdateQuery({ type, id }, deployments),\n  }).data;\n\n  return (\n    <Section\n      title=\"Updates\"\n      icon={<Bell className=\"w-4 h-4\" />}\n      actions={\n        <Link\n          to={`/${usableResourcePath(type as UsableResource)}/${id}/updates`}\n        >\n          <Button variant=\"secondary\" size=\"icon\">\n            <ExternalLink className=\"w-4 h-4\" />\n          </Button>\n        </Link>\n      }\n    >\n      <div className=\"grid md:grid-cols-2 xl:grid-cols-3 gap-4\">\n        {updates?.updates.slice(0, 3).map((update, i) => (\n          <UpdateCard update={update} key={update.id} smallHidden={i > 1} />\n        ))}\n      </div>\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/updates/table.tsx",
    "content": "import { fmt_date_with_minutes, fmt_operation } from \"@lib/formatting\";\nimport { Types } from \"komodo_client\";\nimport { DataTable } from \"@ui/data-table\";\nimport { useState } from \"react\";\nimport { UpdateDetailsInner, UpdateUser } from \"./details\";\nimport { ResourceLink } from \"@components/resources/common\";\nimport { Settings } from \"lucide-react\";\nimport { StatusBadge } from \"@components/util\";\n\nexport const UpdatesTable = ({\n  updates,\n  showTarget,\n}: {\n  updates: Types.UpdateListItem[];\n  showTarget?: boolean;\n}) => {\n  const [id, setId] = useState(\"\");\n  return (\n    <>\n      <DataTable\n        tableKey=\"updates\"\n        data={updates}\n        columns={[\n          {\n            header: \"Operation\",\n            accessorKey: \"operation\",\n            cell: ({ row }) => {\n              const more =\n                row.original.status === Types.UpdateStatus.InProgress\n                  ? \"in progress\"\n                  : row.original.status === Types.UpdateStatus.Queued\n                  ? \"queued\"\n                  : undefined;\n              return (\n                <div className=\"flex items-center gap-2\">\n                  {fmt_operation(row.original.operation)}{\" \"}\n                  {more && (\n                    <div className=\"text-sm text-muted-foreground\">{more}</div>\n                  )}\n                </div>\n              );\n            },\n          },\n          showTarget && {\n            header: \"Target\",\n            cell: ({ row }) =>\n              row.original.target.type === \"System\" ? (\n                <div className=\"flex items-center gap-2\">\n                  <Settings className=\"w-4 h-4\" />\n                  System\n                </div>\n              ) : (\n                <ResourceLink\n                  type={row.original.target.type}\n                  id={row.original.target.id}\n                />\n              ),\n          },\n          {\n            header: \"Result\",\n            cell: ({ row }) => {\n              const { success, status } = row.original;\n              return (\n                <StatusBadge\n                  intent={\n                    status === Types.UpdateStatus.Complete\n                      ? success\n                        ? \"Good\"\n                        : \"Critical\"\n                      : \"None\"\n                  }\n                  text={\n                    status === Types.UpdateStatus.Complete\n                      ? success\n                        ? \"Success\"\n                        : \"Failed\"\n                      : \"Processing\"\n                  }\n                />\n              );\n            },\n          },\n          {\n            header: \"Start Time\",\n            accessorFn: ({ start_ts }) =>\n              fmt_date_with_minutes(new Date(start_ts)),\n          },\n          {\n            header: \"Operator\",\n            accessorKey: \"operator\",\n            cell: ({ row }) => <UpdateUser user_id={row.original.operator} />,\n          },\n        ]}\n        onRowClick={(row) => setId(row.id)}\n      />\n      <UpdateDetailsInner id={id} open={!!id} setOpen={() => setId(\"\")} />\n    </>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/users/delete-user-group.tsx",
    "content": "import { ActionWithDialog } from \"@components/util\";\nimport { useInvalidate, useWrite } from \"@lib/hooks\";\nimport { useToast } from \"@ui/use-toast\";\nimport { Types } from \"komodo_client\";\nimport { Trash } from \"lucide-react\";\nimport { useNavigate } from \"react-router-dom\";\n\nexport const DeleteUserGroup = ({ group }: { group: Types.UserGroup }) => {\n  const nav = useNavigate();\n  const inv = useInvalidate();\n  const { toast } = useToast();\n  const { mutate, isPending } = useWrite(\"DeleteUserGroup\", {\n    onSuccess: () => {\n      inv(\n        [\"ListUserGroups\"],\n        [\"GetUserGroup\", { user_group: group._id?.$oid! }]\n      );\n      toast({ title: `Deleted User Group ${group.name}` });\n      nav(\"/settings\");\n    },\n  });\n\n  return (\n    <ActionWithDialog\n      name={group.name}\n      title=\"Delete\"\n      icon={<Trash className=\"h-4 w-4\" />}\n      variant=\"destructive\"\n      onClick={() => mutate({ id: group._id?.$oid! })}\n      disabled={isPending}\n      loading={isPending}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/users/hooks.ts",
    "content": "import { useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { UsableResource } from \"@types\";\n\nexport const useUserTargetPermissions = (user_target: Types.UserTarget) => {\n  const permissions = useRead(\"ListUserTargetPermissions\", {\n    user_target,\n  }).data;\n  const servers = useRead(\"ListServers\", {}).data;\n  const stacks = useRead(\"ListStacks\", {}).data;\n  const deployments = useRead(\"ListDeployments\", {}).data;\n  const builds = useRead(\"ListBuilds\", {}).data;\n  const repos = useRead(\"ListRepos\", {}).data;\n  const procedures = useRead(\"ListProcedures\", {}).data;\n  const builders = useRead(\"ListBuilders\", {}).data;\n  const alerters = useRead(\"ListAlerters\", {}).data;\n  const syncs = useRead(\"ListResourceSyncs\", {}).data;\n  const perms: (Types.Permission & { name: string })[] = [];\n  addPerms(user_target, permissions, \"Server\", servers, perms);\n  addPerms(user_target, permissions, \"Stack\", stacks, perms);\n  addPerms(user_target, permissions, \"Deployment\", deployments, perms);\n  addPerms(user_target, permissions, \"Build\", builds, perms);\n  addPerms(user_target, permissions, \"Repo\", repos, perms);\n  addPerms(user_target, permissions, \"Procedure\", procedures, perms);\n  addPerms(user_target, permissions, \"Builder\", builders, perms);\n  addPerms(user_target, permissions, \"Alerter\", alerters, perms);\n  addPerms(user_target, permissions, \"ResourceSync\", syncs, perms);\n  return perms;\n};\n\nfunction addPerms<I>(\n  user_target: Types.UserTarget,\n  permissions: Types.Permission[] | undefined,\n  resource_type: UsableResource,\n  resources: Types.ResourceListItem<I>[] | undefined,\n  perms: (Types.Permission & { name: string })[]\n) {\n  resources?.forEach((resource) => {\n    const perm = permissions?.find(\n      (p) =>\n        p.resource_target.type === resource_type &&\n        p.resource_target.id === resource.id\n    );\n    if (perm) {\n      perms.push({ ...perm, name: resource.name });\n    } else {\n      perms.push({\n        user_target,\n        name: resource.name,\n        level: Types.PermissionLevel.None,\n        resource_target: { type: resource_type, id: resource.id },\n      });\n    }\n  });\n}\n"
  },
  {
    "path": "frontend/src/components/users/new.tsx",
    "content": "import { NewLayout } from \"@components/layouts\";\nimport { useInvalidate, useWrite } from \"@lib/hooks\";\nimport { Input } from \"@ui/input\";\nimport { useToast } from \"@ui/use-toast\";\nimport { useState } from \"react\";\n\nexport const NewUserGroup = () => {\n  const { toast } = useToast();\n  const inv = useInvalidate();\n  const { mutateAsync } = useWrite(\"CreateUserGroup\", {\n    onSuccess: () => {\n      inv([\"ListUserGroups\"]);\n      toast({ title: \"Created User Group\" });\n    },\n  });\n  const [name, setName] = useState(\"\");\n  return (\n    <NewLayout\n      entityType=\"User Group\"\n      onConfirm={() => mutateAsync({ name })}\n      enabled={!!name}\n      onOpenChange={() => setName(\"\")}\n    >\n      <div className=\"grid md:grid-cols-2\">\n        Name\n        <Input\n          placeholder=\"user-group-name\"\n          value={name}\n          onChange={(e) => setName(e.target.value)}\n        />\n      </div>\n    </NewLayout>\n  );\n};\n\nexport const NewServiceUser = () => {\n  const { toast } = useToast();\n  const inv = useInvalidate();\n  const { mutateAsync } = useWrite(\"CreateServiceUser\", {\n    onSuccess: () => {\n      inv([\"ListUsers\"]);\n      toast({ title: \"Created Service User\" });\n    },\n  });\n  const [username, setUsername] = useState(\"\");\n  return (\n    <NewLayout\n      entityType=\"Service User\"\n      onConfirm={() => mutateAsync({ username, description: \"\" })}\n      enabled={!!username}\n      onOpenChange={() => setUsername(\"\")}\n    >\n      <div className=\"grid md:grid-cols-2\">\n        Username\n        <Input\n          placeholder=\"username\"\n          value={username}\n          onChange={(e) => setUsername(e.target.value)}\n        />\n      </div>\n    </NewLayout>\n  );\n};\n\nexport const NewLocalUser = () => {\n  const { toast } = useToast();\n  const inv = useInvalidate();\n  const { mutateAsync } = useWrite(\"CreateLocalUser\", {\n    onSuccess: () => {\n      inv([\"ListUsers\"]);\n      toast({ title: \"Created Local User\" });\n    },\n  });\n  const [username, setUsername] = useState(\"\");\n  const [password, setPassword] = useState(\"\");\n  const [passwordConfirm, setPasswordConfirm] = useState(\"\");\n  return (\n    <NewLayout\n      entityType=\"Local User\"\n      configureLabel=\"unique credentials\"\n      onConfirm={async () => {\n        if (\n          username.length === 0 ||\n          password.length === 0 ||\n          password !== passwordConfirm\n        ) {\n          toast({ title: \"Invalid user info\", variant: \"destructive\" });\n        }\n        return await mutateAsync({ username, password });\n      }}\n      enabled={!!username && !!password && password === passwordConfirm}\n      onOpenChange={() => {\n        setUsername(\"\");\n        setPassword(\"\");\n        setPasswordConfirm(\"\");\n      }}\n    >\n      <div className=\"grid md:grid-cols-2 gap-2\">\n        Username\n        <Input\n          placeholder=\"username\"\n          value={username}\n          onChange={(e) => setUsername(e.target.value)}\n        />\n        Password\n        <Input\n          placeholder=\"password\"\n          type=\"password\"\n          value={password}\n          onChange={(e) => setPassword(e.target.value)}\n        />\n        Confirm Password\n        <Input\n          placeholder=\"confirm password\"\n          type=\"password\"\n          value={passwordConfirm}\n          onChange={(e) => setPasswordConfirm(e.target.value)}\n          className={\n            !password\n              ? undefined\n              : password === passwordConfirm\n                ? \"border-green-500\"\n                : \"border-red-500\"\n          }\n        />\n      </div>\n    </NewLayout>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/users/permissions-selector.tsx",
    "content": "import { useState } from \"react\";\nimport { UsableResource } from \"@types\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { Types } from \"komodo_client\";\nimport { Button } from \"@ui/button\";\nimport { filterBySplit } from \"@lib/utils\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport { fmt_upper_camelcase } from \"@lib/formatting\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport { SearchX } from \"lucide-react\";\nimport { Checkbox } from \"@ui/checkbox\";\n\nexport const PermissionLevelSelector = ({\n  level,\n  onSelect,\n  disabled,\n}: {\n  level: Types.PermissionLevel;\n  onSelect: (level: Types.PermissionLevel) => void;\n  disabled?: boolean;\n}) => {\n  return (\n    <Select\n      value={level}\n      onValueChange={(value) => onSelect(value as Types.PermissionLevel)}\n      disabled={disabled}\n    >\n      <SelectTrigger className=\"w-32 capitalize\" disabled={disabled}>\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent className=\"w-32\">\n        {Object.keys(Types.PermissionLevel).map((permission) => (\n          <SelectItem\n            value={permission}\n            key={permission}\n            className=\"capitalize\"\n            disabled={disabled}\n          >\n            {permission}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n};\n\nconst ALL_PERMISSIONS_BY_TYPE: {\n  [type: string]: Types.SpecificPermission[] | undefined;\n} = {\n  Server: [\n    Types.SpecificPermission.Attach,\n    Types.SpecificPermission.Inspect,\n    Types.SpecificPermission.Logs,\n    Types.SpecificPermission.Terminal,\n    Types.SpecificPermission.Processes,\n  ],\n  Stack: [\n    Types.SpecificPermission.Inspect,\n    Types.SpecificPermission.Logs,\n    Types.SpecificPermission.Terminal,\n  ],\n  Deployment: [\n    Types.SpecificPermission.Inspect,\n    Types.SpecificPermission.Logs,\n    Types.SpecificPermission.Terminal,\n  ],\n  Build: [Types.SpecificPermission.Attach],\n  Repo: [Types.SpecificPermission.Attach],\n  Builder: [Types.SpecificPermission.Attach],\n};\n\nexport const SpecificPermissionSelector = ({\n  open,\n  onOpenChange,\n  type,\n  specific,\n  onSelect,\n  disabled,\n}: {\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  type: UsableResource;\n  specific: Types.SpecificPermission[];\n  onSelect: (permission: Types.SpecificPermission) => void;\n  disabled?: boolean;\n}) => {\n  const [search, setSearch] = useState(\"\");\n  const all_permissions = ALL_PERMISSIONS_BY_TYPE[type];\n  // These resources don't have any specific permissions to add\n  if (!all_permissions) {\n    return (\n      <Button\n        variant=\"outline\"\n        className=\"px-3 py-2 rounded-md text-sm w-full justify-start cursor-not-allowed\"\n        disabled\n      >\n        N/a\n      </Button>\n    );\n  }\n  const filtered = filterBySplit(all_permissions, search, (item) => item);\n  return (\n    <Popover open={open} onOpenChange={onOpenChange}>\n      <PopoverTrigger asChild disabled={disabled}>\n        <Button\n          variant=\"outline\"\n          className=\"px-3 py-2 rounded-md text-sm w-full justify-start\"\n          disabled={disabled}\n        >\n          {!specific.length\n            ? \"None\"\n            : specific.map(fmt_upper_camelcase).join(\", \")}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"p-1\" align=\"start\" sideOffset={12}>\n        <Command shouldFilter={false} loop>\n          <CommandInput\n            placeholder={\"Search Permissions\"}\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center pt-2\">\n              No Permissions Found\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n            <CommandGroup>\n              {filtered.map((permission) => (\n                <CommandItem\n                  key={permission}\n                  onSelect={() => onSelect(permission)}\n                  className=\"flex items-center justify-between cursor-pointer\"\n                >\n                  <div className=\"p-1\">{fmt_upper_camelcase(permission)}</div>\n                  <Checkbox checked={specific.includes(permission)} />\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/users/permissions-table.tsx",
    "content": "import { useInvalidate, useRead, useWrite } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { UsableResource } from \"@types\";\nimport { useToast } from \"@ui/use-toast\";\nimport { useState } from \"react\";\nimport { useUserTargetPermissions } from \"./hooks\";\nimport { Section } from \"@components/layouts\";\nimport { Input } from \"@ui/input\";\nimport { ResourceComponents } from \"@components/resources\";\nimport { Label } from \"@ui/label\";\nimport { Switch } from \"@ui/switch\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport {\n  filterBySplit,\n  level_to_number,\n  resource_name,\n  RESOURCE_TARGETS,\n} from \"@lib/utils\";\nimport { ResourceLink } from \"@components/resources/common\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport {\n  PermissionLevelSelector,\n  SpecificPermissionSelector,\n} from \"./permissions-selector\";\n\nexport const PermissionsTableTabs = ({\n  user_target,\n}: {\n  user_target: Types.UserTarget;\n}) => {\n  return (\n    <>\n      <BasePermissionsTable user_target={user_target} />\n      <SpecificPermissionsTable user_target={user_target} />\n    </>\n  );\n};\n\nconst SpecificPermissionsTable = ({\n  user_target,\n}: {\n  user_target: Types.UserTarget;\n}) => {\n  const { toast } = useToast();\n  const [showAll, setShowAll] = useState(false);\n  const [resourceType, setResourceType] = useState<UsableResource | \"All\">(\n    \"All\"\n  );\n  const [search, setSearch] = useState(\"\");\n  const searchSplit = search.toLowerCase().split(\" \");\n  const inv = useInvalidate();\n  const permissions = useUserTargetPermissions(user_target);\n  const { mutate } = useWrite(\"UpdatePermissionOnTarget\", {\n    onSuccess: () => {\n      toast({ title: \"Updated permission\" });\n      inv([\"ListUserTargetPermissions\"]);\n    },\n  });\n  const tableData =\n    permissions?.filter(\n      (permission) =>\n        (resourceType === \"All\"\n          ? true\n          : permission.resource_target.type === resourceType) &&\n        (showAll ? true : permission.level !== Types.PermissionLevel.None) &&\n        searchSplit.every(\n          (search) =>\n            permission.name.toLowerCase().includes(search) ||\n            permission.resource_target.type.toLowerCase().includes(search)\n        )\n    ) ?? [];\n  return (\n    <Section\n      title=\"Per Resource Permissions\"\n      actions={\n        <div className=\"flex gap-6 items-center\">\n          <Input\n            placeholder=\"search\"\n            value={search}\n            onChange={(e) => setSearch(e.target.value)}\n            className=\"w-[300px]\"\n          />\n          <Select\n            value={resourceType}\n            onValueChange={(value) =>\n              setResourceType(value as UsableResource | \"All\")\n            }\n          >\n            <SelectTrigger className=\"w-44\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              {[\"All\", ...Object.keys(ResourceComponents)].map((type) => (\n                <SelectItem key={type} value={type}>\n                  {type === \"All\" ? \"All\" : type + \"s\"}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n          <div\n            className=\"flex gap-3 items-center\"\n            onClick={() => setShowAll((showAll) => !showAll)}\n          >\n            <Label htmlFor=\"show-all\">Show All</Label>\n            <Switch id=\"show-all\" checked={showAll} />\n          </div>\n        </div>\n      }\n    >\n      <DataTable\n        tableKey=\"specific-permissions-v1\"\n        data={tableData}\n        columns={[\n          {\n            accessorKey: \"resource_target.type\",\n            size: 150,\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Resource\" />\n            ),\n            cell: ({ row }) => {\n              const Components =\n                ResourceComponents[\n                  row.original.resource_target.type as UsableResource\n                ];\n              return (\n                <div className=\"flex gap-2 items-center\">\n                  <Components.Icon />\n                  {row.original.resource_target.type}\n                </div>\n              );\n            },\n          },\n          {\n            accessorKey: \"resource_target\",\n            size: 250,\n            sortingFn: (a, b) => {\n              const ra = resource_name(\n                a.original.resource_target.type as UsableResource,\n                a.original.resource_target.id\n              );\n              const rb = resource_name(\n                b.original.resource_target.type as UsableResource,\n                b.original.resource_target.id\n              );\n\n              if (!ra && !rb) return 0;\n              if (!ra) return -1;\n              if (!rb) return 1;\n\n              if (ra > rb) return 1;\n              else if (ra < rb) return -1;\n              else return 0;\n            },\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Target\" />\n            ),\n            cell: ({\n              row: {\n                original: { resource_target },\n              },\n            }) => {\n              return (\n                <ResourceLink\n                  type={resource_target.type as UsableResource}\n                  id={resource_target.id}\n                />\n              );\n            },\n          },\n          {\n            accessorKey: \"level\",\n            size: 150,\n            sortingFn: (a, b) => {\n              const al = level_to_number(a.original.level);\n              const bl = level_to_number(b.original.level);\n              const dif = al - bl;\n              return dif === 0 ? 0 : dif / Math.abs(dif);\n            },\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Level\" />\n            ),\n            cell: ({ row: { original: permission } }) => (\n              <PermissionLevelSelector\n                level={permission.level ?? Types.PermissionLevel.None}\n                onSelect={(value) =>\n                  mutate({\n                    ...permission,\n                    user_target,\n                    permission: {\n                      level: value,\n                      specific: permission.specific ?? [],\n                    },\n                  })\n                }\n              />\n            ),\n          },\n          {\n            header: \"Specific\",\n            size: 300,\n            cell: ({ row: { original: permission } }) => {\n              return (\n                <SpecificPermissionSelector\n                  type={permission.resource_target.type as UsableResource}\n                  specific={permission.specific ?? []}\n                  onSelect={(specific_permission) => {\n                    const _specific = permission.specific ?? [];\n                    const specific = (\n                      _specific.includes(specific_permission)\n                        ? _specific.filter((p) => p !== specific_permission)\n                        : [..._specific, specific_permission]\n                    ).sort();\n                    mutate({\n                      ...permission,\n                      user_target,\n                      permission: {\n                        level: permission.level ?? Types.PermissionLevel.None,\n                        specific,\n                      },\n                    });\n                  }}\n                />\n              );\n            },\n          },\n        ]}\n      />\n    </Section>\n  );\n};\n\ntype UpdateFn = (\n  resource_type: Types.ResourceTarget[\"type\"],\n  permission: Types.PermissionLevelAndSpecifics\n) => void;\n\nconst BasePermissionsTableInner = ({\n  all,\n  update,\n}: {\n  all: Types.User[\"all\"];\n  update: UpdateFn;\n}) => {\n  const [showAll, setShowAll] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const permissions = RESOURCE_TARGETS.map((type) => {\n    const permission = all?.[type] ?? Types.PermissionLevel.None;\n    return {\n      type,\n      level: typeof permission === \"string\" ? permission : permission.level,\n      specific: typeof permission === \"string\" ? [] : permission.specific,\n    };\n  }).filter(\n    (item) =>\n      showAll ||\n      item.level !== Types.PermissionLevel.None ||\n      item.specific.length !== 0\n  );\n  const filtered = filterBySplit(permissions, search, (p) => p.type);\n  return (\n    <Section\n      title=\"Base Permissions on Resource Types\"\n      actions={\n        <div className=\"flex gap-6 items-center\">\n          <Input\n            placeholder=\"search\"\n            value={search}\n            onChange={(e) => setSearch(e.target.value)}\n            className=\"w-[300px]\"\n          />\n          <div\n            className=\"flex gap-3 items-center\"\n            onClick={() => setShowAll((s) => !s)}\n          >\n            <Label htmlFor=\"show-all\">Show All</Label>\n            <Switch id=\"show-all\" checked={showAll} />\n          </div>\n        </div>\n      }\n    >\n      <DataTable\n        tableKey=\"base-permissions-v1\"\n        data={filtered}\n        columns={[\n          {\n            accessorKey: \"type\",\n            size: 150,\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Resource Type\" />\n            ),\n            cell: ({ row }) => {\n              const Components =\n                ResourceComponents[row.original.type as UsableResource];\n              return (\n                <div className=\"flex gap-2 items-center\">\n                  <Components.Icon />\n                  {row.original.type}\n                </div>\n              );\n            },\n          },\n          {\n            accessorKey: \"level\",\n            size: 150,\n            sortingFn: (a, b) => {\n              const al = level_to_number(a.original.level);\n              const bl = level_to_number(b.original.level);\n              const dif = al - bl;\n              return dif === 0 ? 0 : dif / Math.abs(dif);\n            },\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Level\" />\n            ),\n            cell: ({ row }) => (\n              <PermissionLevelSelector\n                level={row.original.level ?? Types.PermissionLevel.None}\n                onSelect={(level) => {\n                  update(row.original.type, {\n                    level,\n                    specific: row.original.specific,\n                  });\n                }}\n              />\n            ),\n          },\n          {\n            header: \"Specific\",\n            size: 300,\n            cell: ({ row }) => {\n              return (\n                <SpecificPermissionSelector\n                  type={row.original.type}\n                  specific={row.original.specific}\n                  onSelect={(specific_permission) => {\n                    const _specific = row.original.specific ?? [];\n                    const specific = (\n                      _specific.includes(specific_permission)\n                        ? _specific.filter((p) => p !== specific_permission)\n                        : [..._specific, specific_permission]\n                    ).sort();\n                    update(row.original.type, {\n                      level: row.original.level,\n                      specific,\n                    });\n                  }}\n                />\n              );\n            },\n          },\n        ]}\n      />\n    </Section>\n  );\n};\n\nconst BasePermissionsTable = ({\n  user_target,\n}: {\n  user_target: Types.UserTarget;\n}) => {\n  const { toast } = useToast();\n  const inv = useInvalidate();\n\n  const { mutate } = useWrite(\"UpdatePermissionOnResourceType\", {\n    onSuccess: () => {\n      toast({ title: \"Updated permissions on target\" });\n      if (user_target.type === \"User\") {\n        inv([\"FindUser\", { user: user_target.id }]);\n      } else if (user_target.type === \"UserGroup\") {\n        inv([\"GetUserGroup\", { user_group: user_target.id }]);\n      }\n    },\n  });\n\n  const update: UpdateFn = (resource_type, permission) =>\n    mutate({ user_target, resource_type, permission });\n\n  if (user_target.type === \"User\") {\n    return (\n      <UserBasePermissionsTable user_id={user_target.id} update={update} />\n    );\n  } else if (user_target.type === \"UserGroup\") {\n    return (\n      <UserGroupBasePermissionsTable\n        group_id={user_target.id}\n        update={update}\n      />\n    );\n  }\n};\n\nconst UserBasePermissionsTable = ({\n  user_id,\n  update,\n}: {\n  user_id: string;\n  update: UpdateFn;\n}) => {\n  const user = useRead(\"FindUser\", { user: user_id }).data;\n  return <BasePermissionsTableInner all={user?.all} update={update} />;\n};\n\nconst UserGroupBasePermissionsTable = ({\n  group_id,\n  update,\n}: {\n  group_id: string;\n  update: UpdateFn;\n}) => {\n  const group = useRead(\"GetUserGroup\", { user_group: group_id }).data;\n  return <BasePermissionsTableInner all={group?.all} update={update} />;\n};\n"
  },
  {
    "path": "frontend/src/components/users/service-api-key.tsx",
    "content": "import { ConfirmButton, CopyButton } from \"@components/util\";\nimport { useInvalidate, useWrite } from \"@lib/hooks\";\nimport { Button } from \"@ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@ui/dropdown-menu\";\nimport { Input } from \"@ui/input\";\nimport { useToast } from \"@ui/use-toast\";\nimport { Check, Loader2, PlusCircle, Trash } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst ONE_DAY_MS = 1000 * 60 * 60 * 24;\n\ntype ExpiresOptions = \"90 days\" | \"180 days\" | \"1 year\" | \"never\";\n\nexport const CreateKeyForServiceUser = ({ user_id }: { user_id: string }) => {\n  const [open, setOpen] = useState(false);\n  const [name, setName] = useState(\"\");\n  const [expires, setExpires] = useState<ExpiresOptions>(\"never\");\n  const [submitted, setSubmitted] = useState<{ key: string; secret: string }>();\n  const invalidate = useInvalidate();\n  const { mutate, isPending } = useWrite(\"CreateApiKeyForServiceUser\", {\n    onSuccess: ({ key, secret }) => {\n      invalidate([\"ListApiKeysForServiceUser\"]);\n      setSubmitted({ key, secret });\n    },\n  });\n  const now = Date.now();\n  const expiresOptions: Record<ExpiresOptions, number> = {\n    \"90 days\": now + ONE_DAY_MS * 90,\n    \"180 days\": now + ONE_DAY_MS * 180,\n    \"1 year\": now + ONE_DAY_MS * 365,\n    never: 0,\n  };\n  const submit = () =>\n    mutate({ user_id, name, expires: expiresOptions[expires] });\n  const onOpenChange = (open: boolean) => {\n    setOpen(open);\n    if (!open) {\n      setName(\"\");\n      setExpires(\"never\");\n      setSubmitted(undefined);\n    }\n  };\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogTrigger asChild>\n        <Button variant=\"secondary\" className=\"items-center gap-2\">\n          New Api Key <PlusCircle className=\"w-4 h-4\" />\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        {submitted ? (\n          <>\n            <DialogHeader>\n              <DialogTitle>Api Key Created</DialogTitle>\n            </DialogHeader>\n            <div className=\"py-8 flex flex-col gap-4\">\n              <div className=\"flex items-center justify-between\">\n                Key\n                <Input className=\"w-72\" value={submitted.key} disabled />\n                <CopyButton content={submitted.key} />\n              </div>\n              <div className=\"flex items-center justify-between\">\n                Secret\n                <Input className=\"w-72\" value={submitted.secret} disabled />\n                <CopyButton content={submitted.secret} />\n              </div>\n            </div>\n            <DialogFooter className=\"flex justify-end\">\n              <Button\n                variant=\"secondary\"\n                className=\"gap-4\"\n                onClick={() => onOpenChange(false)}\n              >\n                Confirm <Check className=\"w-4\" />\n              </Button>\n            </DialogFooter>\n          </>\n        ) : (\n          <>\n            <DialogHeader>\n              <DialogTitle>Create Api Key</DialogTitle>\n            </DialogHeader>\n            <div className=\"py-8 flex flex-col gap-4\">\n              <div className=\"flex items-center justify-between\">\n                Name\n                <Input\n                  className=\"w-72\"\n                  value={name}\n                  onChange={(e) => setName(e.target.value)}\n                />\n              </div>\n              <div className=\"flex items-center justify-between\">\n                Expiry\n                <DropdownMenu>\n                  <DropdownMenuTrigger asChild>\n                    <Button\n                      variant=\"outline\"\n                      className=\"w-36 justify-between px-3\"\n                    >\n                      {expires}\n                    </Button>\n                  </DropdownMenuTrigger>\n                  <DropdownMenuContent className=\"w-36\" side=\"bottom\">\n                    <DropdownMenuGroup>\n                      {Object.keys(expiresOptions)\n                        .filter((option) => option !== expires)\n                        .map((option) => (\n                          <DropdownMenuItem\n                            key={option}\n                            onClick={() => setExpires(option as any)}\n                          >\n                            {option}\n                          </DropdownMenuItem>\n                        ))}\n                    </DropdownMenuGroup>\n                  </DropdownMenuContent>\n                </DropdownMenu>\n              </div>\n            </div>\n            <DialogFooter className=\"flex justify-end\">\n              <Button\n                variant=\"secondary\"\n                className=\"gap-4\"\n                onClick={submit}\n                disabled={isPending}\n              >\n                Submit\n                {isPending ? (\n                  <Loader2 className=\"w-4 animate-spin\" />\n                ) : (\n                  <Check className=\"w-4\" />\n                )}\n              </Button>\n            </DialogFooter>\n          </>\n        )}\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const DeleteKeyForServiceUser = ({ api_key }: { api_key: string }) => {\n  const inv = useInvalidate();\n  const { toast } = useToast();\n  const { mutate, isPending } = useWrite(\"DeleteApiKeyForServiceUser\", {\n    onSuccess: () => {\n      inv([\"ListApiKeysForServiceUser\"]);\n      toast({ title: \"Api Key Deleted\" });\n    },\n    onError: () => {\n      toast({ title: \"Failed to delete api key\", variant: \"destructive\" });\n    },\n  });\n  return (\n    <ConfirmButton\n      title=\"Delete\"\n      icon={<Trash className=\"w-4 h-4\" />}\n      onClick={(e) => {\n        e.stopPropagation();\n        mutate({ key: api_key });\n      }}\n      loading={isPending}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/users/table.tsx",
    "content": "import { text_color_class_by_intention } from \"@lib/color\";\nimport { Types } from \"komodo_client\";\nimport { DataTable } from \"@ui/data-table\";\nimport { useNavigate } from \"react-router-dom\";\nimport { ColumnDef } from \"@tanstack/react-table\";\nimport { MinusCircle } from \"lucide-react\";\nimport { ConfirmButton } from \"@components/util\";\nimport { useUser } from \"@lib/hooks\";\n\nexport const UserTable = ({\n  users,\n  onUserRemove,\n  onUserDelete,\n  userDeleteDisabled,\n  onSelfClick,\n}: {\n  users: Types.User[];\n  onUserRemove?: (user_id: string) => void;\n  onUserDelete?: (user_id: string) => void;\n  userDeleteDisabled?: (user_id: string) => boolean;\n  onSelfClick?: () => void;\n}) => {\n  const user = useUser().data;\n  const nav = useNavigate();\n  const columns: ColumnDef<Types.User, \"User\" | \"Admin\" | \"Super Admin\">[] = [\n    { header: \"Username\", accessorKey: \"username\" },\n    { header: \"Type\", accessorKey: \"config.type\" },\n    {\n      header: \"Level\",\n      accessorFn: (user) =>\n        user.admin ? (user.super_admin ? \"Super Admin\" : \"Admin\") : \"User\",\n    },\n    {\n      header: \"Enabled\",\n      cell: ({ row }) => {\n        const enabledClass = row.original.enabled\n          ? text_color_class_by_intention(\"Good\")\n          : text_color_class_by_intention(\"Critical\");\n        return (\n          <div className={enabledClass}>\n            {row.original.enabled ? \"Enabled\" : \"Disabled\"}\n          </div>\n        );\n      },\n    },\n  ];\n  if (onUserRemove) {\n    columns.push({\n      header: \"Remove\",\n      cell: ({ row }) => (\n        <ConfirmButton\n          title=\"Remove\"\n          variant=\"destructive\"\n          icon={<MinusCircle className=\"w-4 h-4\" />}\n          onClick={(e) => {\n            e.stopPropagation();\n            onUserRemove(row.original._id?.$oid!);\n          }}\n        />\n      ),\n    });\n  }\n  if (onUserDelete) {\n    columns.push({\n      header: \"Delete\",\n      cell: ({ row }) => (\n        <ConfirmButton\n          title=\"Delete\"\n          variant=\"destructive\"\n          icon={<MinusCircle className=\"w-4 h-4\" />}\n          onClick={(e) => {\n            e.stopPropagation();\n            onUserDelete(row.original._id?.$oid!);\n          }}\n          disabled={\n            row.original._id?.$oid\n              ? userDeleteDisabled?.(row.original._id.$oid) ?? true\n              : true\n          }\n        />\n      ),\n    });\n  }\n  return (\n    <DataTable\n      tableKey=\"users\"\n      data={users}\n      columns={columns}\n      onRowClick={(row) =>\n        row._id?.$oid === user?._id?.$oid\n          ? onSelfClick?.()\n          : nav(`/users/${row._id!.$oid}`)\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/util.tsx",
    "content": "import {\n  Dispatch,\n  FocusEventHandler,\n  Fragment,\n  MouseEventHandler,\n  ReactNode,\n  SetStateAction,\n  forwardRef,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"../ui/button\";\nimport {\n  Box,\n  Check,\n  CheckCircle,\n  ChevronDown,\n  ChevronLeft,\n  ChevronsUpDown,\n  ChevronUp,\n  Copy,\n  Database,\n  EthernetPort,\n  FolderGit,\n  HardDrive,\n  LinkIcon,\n  Loader2,\n  Network,\n  Search,\n  SearchX,\n  Settings,\n  Tags,\n  User,\n} from \"lucide-react\";\nimport { Input } from \"../ui/input\";\nimport {\n  Dialog,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n  DialogContent,\n  DialogFooter,\n} from \"@ui/dialog\";\nimport { toast, useToast } from \"@ui/use-toast\";\nimport { cn, filterBySplit, usableResourcePath } from \"@lib/utils\";\nimport { Link, useNavigate } from \"react-router-dom\";\nimport { Textarea } from \"@ui/textarea\";\nimport { Card } from \"@ui/card\";\nimport {\n  fmt_port_mount,\n  fmt_resource_type,\n  fmt_utc_offset,\n  snake_case_to_upper_space_case,\n} from \"@lib/formatting\";\nimport {\n  ColorIntention,\n  container_state_intention,\n  hex_color_by_intention,\n  stroke_color_class_by_intention,\n  text_color_class_by_intention,\n} from \"@lib/color\";\nimport { Types } from \"komodo_client\";\nimport { Badge } from \"@ui/badge\";\nimport { Section } from \"./layouts\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport {\n  useContainerPortsMap,\n  useRead,\n  useTemplatesQueryBehavior,\n  usePromptHotkeys,\n} from \"@lib/hooks\";\nimport { Prune } from \"./resources/server/actions\";\nimport { MonacoEditor, MonacoLanguage } from \"./monaco\";\nimport { UsableResource } from \"@types\";\nimport { ResourceComponents } from \"./resources\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@ui/tooltip\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { useServer } from \"./resources/server\";\n\nexport const ActionButton = forwardRef<\n  HTMLButtonElement,\n  {\n    variant?:\n      | \"link\"\n      | \"default\"\n      | \"destructive\"\n      | \"outline\"\n      | \"secondary\"\n      | \"ghost\"\n      | null\n      | undefined;\n    size?: \"default\" | \"sm\" | \"lg\" | \"icon\" | null | undefined;\n    title: string;\n    icon: ReactNode;\n    disabled?: boolean;\n    className?: string;\n    onClick?: MouseEventHandler<HTMLButtonElement>;\n    onBlur?: FocusEventHandler<HTMLButtonElement>;\n    loading?: boolean;\n    \"data-confirm-button\"?: boolean;\n  }\n>(\n  (\n    {\n      variant,\n      size,\n      title,\n      icon,\n      disabled,\n      className,\n      loading,\n      onClick,\n      onBlur,\n      \"data-confirm-button\": dataConfirmButton,\n    },\n    ref\n  ) => (\n    <Button\n      size={size}\n      variant={variant || \"secondary\"}\n      className={cn(\n        \"flex flex-1 shrink-0 gap-4 items-center justify-between max-w-[190px]\",\n        className\n      )}\n      onClick={onClick}\n      onBlur={onBlur}\n      disabled={disabled || loading}\n      ref={ref}\n      data-confirm-button={dataConfirmButton}\n    >\n      {title} {loading ? <Loader2 className=\"w-4 h-4 animate-spin\" /> : icon}\n    </Button>\n  )\n);\n\nexport const ActionWithDialog = ({\n  name,\n  title,\n  icon,\n  disabled,\n  loading,\n  onClick,\n  additional,\n  targetClassName,\n  variant,\n  forceConfirmDialog,\n}: {\n  name: string;\n  title: string;\n  icon: ReactNode;\n  disabled?: boolean;\n  loading?: boolean;\n  onClick?: () => void;\n  additional?: ReactNode;\n  targetClassName?: string;\n  variant?:\n    | \"link\"\n    | \"default\"\n    | \"destructive\"\n    | \"outline\"\n    | \"secondary\"\n    | \"ghost\"\n    | null\n    | undefined;\n  /**\n   * For some ops (Delete), force confirm dialog\n   * even if disabled.\n   */\n  forceConfirmDialog?: boolean;\n}) => {\n  const disable_confirm_dialog =\n    useRead(\"GetCoreInfo\", {}).data?.disable_confirm_dialog ?? false;\n  const [open, setOpen] = useState(false);\n  const [input, setInput] = useState(\"\");\n  const confirmButtonRef = useRef<HTMLButtonElement>(null);\n\n  // Add prompt hotkeys for better UX when dialog is open\n  usePromptHotkeys({\n    onConfirm: () => {\n      if (name === input && !disabled) {\n        onClick && onClick();\n        setOpen(false);\n      }\n    },\n    onCancel: () => setOpen(false),\n    enabled: open,\n    confirmDisabled: disabled || name !== input,\n  });\n\n  // If confirm dialogs are disabled and this isn't forced, use ConfirmButton directly\n  if (!forceConfirmDialog && disable_confirm_dialog) {\n    return (\n      <ConfirmButton\n        variant={variant}\n        title={title}\n        icon={icon}\n        disabled={disabled}\n        loading={loading}\n        className={targetClassName}\n        onClick={onClick}\n      />\n    );\n  }\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(open) => {\n        setOpen(open);\n        setInput(\"\");\n      }}\n    >\n      <DialogTrigger asChild>\n        <ActionButton\n          className={targetClassName}\n          title={title}\n          icon={icon}\n          disabled={disabled}\n          onClick={() => setOpen(true)}\n          loading={loading}\n          variant={variant}\n        />\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Confirm {title}</DialogTitle>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-4 my-4\">\n          <p\n            onClick={() => {\n              navigator.clipboard.writeText(name);\n              toast({ title: `Copied \"${name}\" to clipboard!` });\n            }}\n            className=\"cursor-pointer\"\n          >\n            Please enter <b>{name}</b> below to confirm this action.\n            <br />\n            <span className=\"text-xs text-muted-foreground\">\n              You may click the name in bold to copy it\n            </span>\n          </p>\n          <Input value={input} onChange={(e) => setInput(e.target.value)} />\n          {additional}\n        </div>\n        <DialogFooter>\n          <ConfirmButton\n            ref={confirmButtonRef}\n            title={title}\n            icon={icon}\n            disabled={disabled || name !== input}\n            onClick={() => {\n              onClick && onClick();\n              setOpen(false);\n            }}\n          />\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const ConfirmButton = forwardRef<\n  HTMLButtonElement,\n  {\n    variant?:\n      | \"link\"\n      | \"default\"\n      | \"destructive\"\n      | \"outline\"\n      | \"secondary\"\n      | \"ghost\"\n      | null\n      | undefined;\n    size?: \"default\" | \"sm\" | \"lg\" | \"icon\" | null | undefined;\n    title: string;\n    icon: ReactNode;\n    onClick?: MouseEventHandler<HTMLButtonElement>;\n    loading?: boolean;\n    disabled?: boolean;\n    className?: string;\n  }\n>(({\n  variant,\n  size,\n  title,\n  icon,\n  disabled,\n  loading,\n  onClick,\n  className,\n}, ref) => {\n  const [confirmed, set] = useState(false);\n\n  return (\n    <ActionButton\n      ref={ref}\n      variant={variant}\n      size={size}\n      title={confirmed ? \"Confirm\" : title}\n      icon={confirmed ? <Check className=\"w-4 h-4\" /> : icon}\n      disabled={disabled}\n      onClick={\n        confirmed\n          ? (e) => {\n              e.stopPropagation();\n              onClick && onClick(e);\n              set(false);\n            }\n          : (e) => {\n              e.stopPropagation();\n              set(true);\n            }\n      }\n      onBlur={() => set(false)}\n      loading={loading}\n      className={className}\n      data-confirm-button={true}\n    />\n  );\n});\n\nexport const UserSettings = () => (\n  <Link to=\"/settings\">\n    <Button variant=\"ghost\" size=\"icon\">\n      <Settings className=\"w-4 h-4\" />\n    </Button>\n  </Link>\n);\n\nexport const CopyButton = ({\n  content,\n  className,\n  icon = <Copy className=\"w-4 h-4\" />,\n  label = \"selection\",\n}: {\n  content: string | undefined;\n  className?: string;\n  icon?: ReactNode;\n  label?: string;\n}) => {\n  const { toast } = useToast();\n  const [copied, set] = useState(false);\n\n  useEffect(() => {\n    if (copied) {\n      toast({ title: \"Copied \" + label });\n      const timeout = setTimeout(() => set(false), 3000);\n      return () => {\n        clearTimeout(timeout);\n      };\n    }\n  }, [content, copied, toast]);\n\n  return (\n    <Button\n      className={cn(\"shrink-0\", className)}\n      size=\"icon\"\n      variant=\"outline\"\n      onClick={() => {\n        if (!content) return;\n        navigator.clipboard.writeText(content);\n        set(true);\n      }}\n      disabled={!content}\n    >\n      {copied ? <Check className=\"w-4 h-4\" /> : icon}\n    </Button>\n  );\n};\n\nexport const TextUpdateMenuMonaco = ({\n  title,\n  titleRight,\n  value = \"\",\n  triggerClassName,\n  onUpdate,\n  placeholder,\n  confirmButton,\n  disabled,\n  fullWidth,\n  open,\n  setOpen,\n  triggerHidden,\n  language,\n  triggerChild,\n}: {\n  title: string;\n  titleRight?: ReactNode;\n  value: string | undefined;\n  onUpdate: (value: string) => void;\n  triggerClassName?: string;\n  placeholder?: string;\n  confirmButton?: boolean;\n  disabled?: boolean;\n  fullWidth?: boolean;\n  open?: boolean;\n  setOpen?: (open: boolean) => void;\n  triggerHidden?: boolean;\n  language?: MonacoLanguage;\n  triggerChild?: ReactNode;\n}) => {\n  const [_open, _setOpen] = useState(false);\n  const [__open, __setOpen] = [open ?? _open, setOpen ?? _setOpen];\n  const [_value, setValue] = useState(value);\n  useEffect(() => setValue(value), [value]);\n  const onClick = () => {\n    onUpdate(_value);\n    __setOpen(false);\n  };\n\n  return (\n    <Dialog open={__open} onOpenChange={__setOpen}>\n      <DialogTrigger asChild>\n        {triggerChild ?? (\n          <Card\n            className={cn(\n              \"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer\",\n              fullWidth ? \"w-full\" : \"w-fit\",\n              triggerHidden && \"hidden\"\n            )}\n          >\n            <div\n              className={cn(\n                \"text-sm text-nowrap overflow-hidden overflow-ellipsis\",\n                (!value || !!disabled) && \"text-muted-foreground\",\n                triggerClassName\n              )}\n            >\n              {value.split(\"\\n\")[0] || placeholder}\n            </div>\n          </Card>\n        )}\n      </DialogTrigger>\n      <DialogContent className=\"min-w-[50vw]\">\n        {titleRight && (\n          <div className=\"flex items-center gap-4\">\n            <DialogHeader>\n              <DialogTitle>{title}</DialogTitle>\n            </DialogHeader>\n            {titleRight}\n          </div>\n        )}\n        {!titleRight && (\n          <DialogHeader>\n            <DialogTitle>{title}</DialogTitle>\n          </DialogHeader>\n        )}\n\n        <MonacoEditor\n          value={_value}\n          language={language}\n          onValueChange={setValue}\n          readOnly={disabled}\n        />\n\n        {!disabled && (\n          <DialogFooter>\n            {confirmButton ? (\n              <ConfirmButton\n                title=\"Update\"\n                icon={<CheckCircle className=\"w-4 h-4\" />}\n                onClick={onClick}\n              />\n            ) : (\n              <Button\n                variant=\"secondary\"\n                onClick={onClick}\n                className=\"flex items-center gap-2\"\n              >\n                <CheckCircle className=\"w-4 h-4\" />\n                Update\n              </Button>\n            )}\n          </DialogFooter>\n        )}\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const UserAvatar = ({\n  avatar,\n  size = 4,\n}: {\n  avatar: string | undefined;\n  size?: number;\n}) =>\n  avatar ? (\n    <img src={avatar} alt=\"Avatar\" className={`w-${size} h-${size}`} />\n  ) : (\n    <User className={`w-${size} h-${size}`} />\n  );\n\nexport const StatusBadge = ({\n  text,\n  intent,\n}: {\n  text: string | undefined;\n  intent: ColorIntention;\n}) => {\n  if (!text) return null;\n\n  const color = text_color_class_by_intention(intent);\n  const background = hex_color_by_intention(intent) + \"25\";\n\n  const _text = text === Types.ServerState.NotOk ? \"Not Ok\" : text;\n  const displayText = snake_case_to_upper_space_case(_text).toUpperCase();\n\n  // Special handling for \"VERSION MISMATCH\" with flex layout for responsive design\n  if (displayText === \"VERSION MISMATCH\") {\n    return (\n      <div\n        className={cn(\n          \"px-2 py-1 text-xs text-white rounded-md font-medium tracking-wide\",\n          \"inline-flex flex-wrap items-center justify-center text-center\",\n          \"leading-tight gap-x-1\",\n          \"min-h-[1.5rem]\", // Minimum height to match other badges, but can grow\n          color\n        )}\n        style={{ \n          background,\n          minWidth: \"fit-content\",\n          maxWidth: \"80px\", // This controls when it wraps to two lines\n        }}\n      >\n        <span>VERSION</span>\n        <span>MISMATCH</span>\n      </div>\n    );\n  }\n\n  return (\n    <p\n      className={cn(\n        \"px-2 py-1 w-fit text-xs text-white rounded-md font-medium tracking-wide\",\n        \"h-6 flex items-center\", // Fixed height and center content vertically\n        color\n      )}\n      style={{ background }}\n    >\n      {displayText}\n    </p>\n  );\n};\n\nexport const DockerOptions = ({\n  options,\n}: {\n  options: Record<string, string> | undefined;\n}) => {\n  if (!options) return null;\n  const entries = Object.entries(options);\n  if (entries.length === 0) return null;\n  return (\n    <div className=\"flex gap-2 flex-wrap\">\n      {entries.map(([key, value]) => (\n        <Badge key={key} variant=\"secondary\">\n          {key} = {value}\n        </Badge>\n      ))}\n    </div>\n  );\n};\n\nexport const DockerLabelsSection = ({\n  labels,\n}: {\n  labels: Record<string, string> | undefined;\n}) => {\n  if (!labels) return null;\n  const entries = Object.entries(labels);\n  if (entries.length === 0) return null;\n  return (\n    <Section title=\"Labels\" icon={<Tags className=\"w-4 h-4\" />}>\n      <div className=\"flex gap-2 flex-wrap\">\n        {entries.map(([key, value]) => (\n          <Badge key={key} variant=\"secondary\" className=\"flex gap-1\">\n            <span className=\"text-muted-foreground\">{key}</span>\n            <span className=\"text-muted-foreground\">=</span>\n            <span\n              title={value}\n              className=\"font-extrabold text-nowrap max-w-[200px] overflow-hidden text-ellipsis\"\n            >\n              {value}\n            </span>\n          </Badge>\n        ))}\n      </div>\n    </Section>\n  );\n};\n\nexport const ShowHideButton = ({\n  show,\n  setShow,\n}: {\n  show: boolean;\n  setShow: (show: boolean) => void;\n}) => {\n  return (\n    <Button\n      size=\"sm\"\n      variant=\"outline\"\n      className=\"gap-4\"\n      onClick={() => setShow(!show)}\n    >\n      {show ? \"Hide\" : \"Show\"}\n      {show ? <ChevronUp className=\"w-4\" /> : <ChevronDown className=\"w-4\" />}\n    </Button>\n  );\n};\n\ntype DockerResourceType = \"container\" | \"network\" | \"image\" | \"volume\";\n\nexport const DOCKER_LINK_ICONS: {\n  [type in DockerResourceType]: React.FC<{\n    server_id: string;\n    name: string | undefined;\n    size?: number;\n  }>;\n} = {\n  container: ({ server_id, name, size = 4 }) => {\n    const state =\n      useRead(\"ListDockerContainers\", { server: server_id }).data?.find(\n        (container) => container.name === name\n      )?.state ?? Types.ContainerStateStatusEnum.Empty;\n    return (\n      <Box\n        className={cn(\n          `w-${size} h-${size}`,\n          stroke_color_class_by_intention(container_state_intention(state))\n        )}\n      />\n    );\n  },\n  network: ({ server_id, name, size = 4 }) => {\n    const containers =\n      useRead(\"ListDockerContainers\", { server: server_id }).data ?? [];\n    const no_containers = !name\n      ? false\n      : containers.every((container) => !container.networks?.includes(name));\n    return (\n      <Network\n        className={cn(\n          `w-${size} h-${size}`,\n          stroke_color_class_by_intention(\n            !name\n              ? \"Warning\"\n              : no_containers\n                ? [\"none\", \"host\", \"bridge\"].includes(name)\n                  ? \"None\"\n                  : \"Critical\"\n                : \"Good\"\n          )\n        )}\n      />\n    );\n  },\n  image: ({ server_id, name, size = 4 }) => {\n    const containers =\n      useRead(\"ListDockerContainers\", { server: server_id }).data ?? [];\n    const no_containers = !name\n      ? false\n      : containers.every((container) => container.image_id !== name);\n    return (\n      <HardDrive\n        className={cn(\n          `w-${size} h-${size}`,\n          stroke_color_class_by_intention(\n            !name ? \"Warning\" : no_containers ? \"Critical\" : \"Good\"\n          )\n        )}\n      />\n    );\n  },\n  volume: ({ server_id, name, size = 4 }) => {\n    const containers =\n      useRead(\"ListDockerContainers\", { server: server_id }).data ?? [];\n    const no_containers = !name\n      ? false\n      : containers.every((container) => !container.volumes?.includes(name));\n    return (\n      <Database\n        className={cn(\n          `w-${size} h-${size}`,\n          stroke_color_class_by_intention(\n            !name ? \"Warning\" : no_containers ? \"Critical\" : \"Good\"\n          )\n        )}\n      />\n    );\n  },\n};\n\nexport const DockerResourceLink = ({\n  server_id,\n  name,\n  id,\n  type,\n  extra,\n  muted,\n}: {\n  server_id: string;\n  name: string | undefined;\n  id?: string;\n  type: \"container\" | \"network\" | \"image\" | \"volume\";\n  extra?: ReactNode;\n  muted?: boolean;\n}) => {\n  if (!name) return \"Unknown\";\n\n  const Icon = DOCKER_LINK_ICONS[type];\n\n  return (\n    <Link\n      to={`/servers/${server_id}/${type}/${encodeURIComponent(name)}`}\n      className={cn(\n        \"flex items-center gap-2 text-sm hover:underline py-1\",\n        muted && \"text-muted-foreground\"\n      )}\n    >\n      <Icon server_id={server_id} name={type === \"image\" ? id : name} />\n      <div\n        title={name}\n        className=\"max-w-[250px] lg:max-w-[300px] overflow-hidden overflow-ellipsis break-words\"\n      >\n        {name}\n      </div>\n      {extra && <div className=\"no-underline\">{extra}</div>}\n    </Link>\n  );\n};\n\nexport const DockerResourcePageName = ({ name: _name }: { name?: string }) => {\n  const name = _name ?? \"Unknown\";\n  return (\n    <h1\n      title={name}\n      className=\"text-3xl max-w-[300px] md:max-w-[500px] xl:max-w-[700px] overflow-hidden overflow-ellipsis\"\n    >\n      {name}\n    </h1>\n  );\n};\n\nexport const DockerContainersSection = ({\n  server_id,\n  containers,\n  show = true,\n  setShow,\n  pruneButton,\n  titleOther,\n  forceTall,\n  _search,\n}: {\n  server_id: string;\n  containers: Types.ListDockerContainersResponse;\n  show?: boolean;\n  setShow?: (show: boolean) => void;\n  pruneButton?: boolean;\n  titleOther?: ReactNode;\n  forceTall?: boolean;\n  _search?: [string, Dispatch<SetStateAction<string>>];\n}) => {\n  const allRunning = useRead(\"ListDockerContainers\", {\n    server: server_id,\n  }).data?.every(\n    (container) => container.state === Types.ContainerStateStatusEnum.Running\n  );\n  const filtered = _search\n    ? filterBySplit(containers, _search[0], (container) => container.name)\n    : containers;\n  return (\n    <div className={cn(setShow && show && \"mb-8\")}>\n      <Section\n        titleOther={titleOther}\n        title={!titleOther ? \"Containers\" : undefined}\n        icon={!titleOther ? <Box className=\"w-4 h-4\" /> : undefined}\n        actions={\n          <div className=\"flex items-center gap-4\">\n            {pruneButton && !allRunning && (\n              <Prune server_id={server_id} type=\"Containers\" />\n            )}\n            {_search && (\n              <div className=\"relative\">\n                <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n                <Input\n                  value={_search[0]}\n                  onChange={(e) => _search[1](e.target.value)}\n                  placeholder=\"search...\"\n                  className=\"pl-8 w-[200px] lg:w-[300px]\"\n                />\n              </div>\n            )}\n            {setShow && <ShowHideButton show={show} setShow={setShow} />}\n          </div>\n        }\n      >\n        {show && (\n          <DataTable\n            containerClassName={forceTall ? \"min-h-[60vh]\" : undefined}\n            tableKey=\"server-containers\"\n            data={filtered}\n            columns={[\n              {\n                accessorKey: \"name\",\n                size: 260,\n                header: ({ column }) => (\n                  <SortableHeader column={column} title=\"Name\" />\n                ),\n                cell: ({ row }) => (\n                  <DockerResourceLink\n                    type=\"container\"\n                    server_id={server_id}\n                    name={row.original.name}\n                  />\n                ),\n              },\n              {\n                accessorKey: \"state\",\n                size: 160,\n                header: ({ column }) => (\n                  <SortableHeader column={column} title=\"State\" />\n                ),\n                cell: ({ row }) => {\n                  const state = row.original?.state;\n                  return (\n                    <StatusBadge\n                      text={state}\n                      intent={container_state_intention(state)}\n                    />\n                  );\n                },\n              },\n              {\n                accessorKey: \"image\",\n                size: 300,\n                header: ({ column }) => (\n                  <SortableHeader column={column} title=\"Image\" />\n                ),\n                cell: ({ row }) => (\n                  <DockerResourceLink\n                    type=\"image\"\n                    server_id={server_id}\n                    name={row.original.image}\n                    id={row.original.image_id}\n                  />\n                ),\n              },\n              {\n                accessorKey: \"networks.0\",\n                size: 200,\n                header: ({ column }) => (\n                  <SortableHeader column={column} title=\"Networks\" />\n                ),\n                cell: ({ row }) =>\n                  (row.original.networks?.length ?? 0) > 0 ? (\n                    <div className=\"flex items-center gap-x-2 flex-wrap\">\n                      {row.original.networks?.map((network, i) => (\n                        <Fragment key={network}>\n                          <DockerResourceLink\n                            type=\"network\"\n                            server_id={server_id}\n                            name={network}\n                          />\n                          {i !== row.original.networks!.length - 1 && (\n                            <div className=\"text-muted-foreground\">|</div>\n                          )}\n                        </Fragment>\n                      ))}\n                    </div>\n                  ) : (\n                    row.original.network_mode && (\n                      <DockerResourceLink\n                        type=\"network\"\n                        server_id={server_id}\n                        name={row.original.network_mode}\n                      />\n                    )\n                  ),\n              },\n              {\n                accessorKey: \"ports.0\",\n                size: 200,\n                sortingFn: (a, b) => {\n                  const getMinHostPort = (row: typeof a) => {\n                    const ports = row.original.ports ?? [];\n                    if (!ports.length) return Number.POSITIVE_INFINITY;\n                    const nums = ports\n                      .map((p) => p.PublicPort)\n                      .filter((p): p is number => typeof p === \"number\")\n                      .map((n) => Number(n));\n                    if (!nums.length || nums.some((n) => Number.isNaN(n))) {\n                      return Number.POSITIVE_INFINITY;\n                    }\n                    return Math.min(...nums);\n                  };\n                  const pa = getMinHostPort(a);\n                  const pb = getMinHostPort(b);\n                  return pa === pb ? 0 : pa > pb ? 1 : -1;\n                },\n                header: ({ column }) => (\n                  <SortableHeader column={column} title=\"Ports\" />\n                ),\n                cell: ({ row }) => (\n                  <ContainerPortsTableView\n                    ports={row.original.ports ?? []}\n                    server_id={row.original.server_id}\n                  />\n                ),\n              },\n            ]}\n          />\n        )}\n      </Section>\n    </div>\n  );\n};\n\nexport const TextUpdateMenuSimple = ({\n  title,\n  titleRight,\n  value = \"\",\n  triggerClassName,\n  onUpdate,\n  placeholder,\n  confirmButton,\n  disabled,\n  open,\n  setOpen,\n}: {\n  title: string;\n  titleRight?: ReactNode;\n  value: string | undefined;\n  onUpdate: (value: string) => void;\n  triggerClassName?: string;\n  placeholder?: string;\n  confirmButton?: boolean;\n  disabled?: boolean;\n  open?: boolean;\n  setOpen?: (open: boolean) => void;\n}) => {\n  const [_open, _setOpen] = useState(false);\n  const [__open, __setOpen] = [open ?? _open, setOpen ?? _setOpen];\n  const [_value, setValue] = useState(value);\n  useEffect(() => setValue(value), [value]);\n  const onClick = () => {\n    onUpdate(_value);\n    __setOpen(false);\n  };\n\n  return (\n    <Dialog open={__open} onOpenChange={__setOpen}>\n      <DialogTrigger asChild>\n        <div\n          className={cn(\n            \"text-sm text-nowrap overflow-hidden overflow-ellipsis p-2 border rounded-md flex-1 cursor-pointer hover:bg-accent/25\",\n            (!value || !!disabled) && \"text-muted-foreground\",\n            triggerClassName\n          )}\n        >\n          {value.split(\"\\n\")[0] || placeholder}\n        </div>\n      </DialogTrigger>\n      <DialogContent className=\"min-w-[50vw]\">\n        {titleRight && (\n          <div className=\"flex items-center gap-4\">\n            <DialogHeader>\n              <DialogTitle>{title}</DialogTitle>\n            </DialogHeader>\n            {titleRight}\n          </div>\n        )}\n        {!titleRight && (\n          <DialogHeader>\n            <DialogTitle>{title}</DialogTitle>\n          </DialogHeader>\n        )}\n\n        <Textarea\n          value={_value}\n          onChange={(e) => setValue(e.target.value)}\n          placeholder={placeholder}\n          className=\"min-h-[200px]\"\n          disabled={disabled}\n        />\n        {!disabled && (\n          <DialogFooter>\n            {confirmButton ? (\n              <ConfirmButton\n                title=\"Update\"\n                icon={<CheckCircle className=\"w-4 h-4\" />}\n                onClick={onClick}\n              />\n            ) : (\n              <Button\n                variant=\"secondary\"\n                onClick={onClick}\n                className=\"flex items-center gap-2\"\n              >\n                <CheckCircle className=\"w-4 h-4\" />\n                Update\n              </Button>\n            )}\n          </DialogFooter>\n        )}\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const NotFound = ({ type }: { type: UsableResource | undefined }) => {\n  const nav = useNavigate();\n  const Components = type && ResourceComponents[type];\n  return (\n    <div className=\"flex flex-col gap-4\">\n      {type && (\n        <div className=\"flex items-center justify-between mb-4\">\n          <Button\n            className=\"gap-2\"\n            variant=\"secondary\"\n            onClick={() => nav(\"/\" + usableResourcePath(type))}\n          >\n            <ChevronLeft className=\"w-4\" /> Back\n          </Button>\n        </div>\n      )}\n      <div className=\"grid lg:grid-cols-2 gap-4\">\n        <div className=\"flex items-center gap-4\">\n          <div className=\"mt-1\">\n            {Components ? (\n              <Components.BigIcon />\n            ) : (\n              <SearchX className=\"w-8 h-8\" />\n            )}\n          </div>\n          <h1 className=\"text-3xl font-mono\">\n            {type} {type && \" - \"} 404 Not Found\n          </h1>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport const RepoLink = ({ repo, link }: { repo: string; link: string }) => {\n  return (\n    <a\n      target=\"_blank\"\n      href={link}\n      className=\"text-sm cursor-pointer hover:underline\"\n    >\n      <div className=\"flex items-center gap-2\">\n        <FolderGit className=\"w-4 h-4\" />\n        {repo}\n      </div>\n    </a>\n  );\n};\n\nconst TIMEZONES: (\"Default\" | Types.IanaTimezone)[] = [\n  \"Default\",\n  ...Object.values(Types.IanaTimezone),\n];\n\nexport const TimezoneSelector = ({\n  timezone,\n  onChange,\n  disabled,\n  triggerClassName,\n}: {\n  timezone: string;\n  onChange: (timezone: \"\" | Types.IanaTimezone) => void;\n  disabled?: boolean;\n  triggerClassName?: string;\n}) => {\n  const core_tz = useRead(\"GetCoreInfo\", {}).data?.timezone || \"Core TZ\";\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const filtered = filterBySplit(TIMEZONES, search, (t) => t);\n\n  return (\n    <Popover open={open} onOpenChange={setOpen} modal>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"secondary\"\n          className={cn(\n            \"flex justify-between gap-2 w-[300px]\",\n            triggerClassName\n          )}\n          disabled={disabled}\n        >\n          {timezone\n            ? `${timezone} (${fmt_utc_offset(timezone as Types.IanaTimezone)})`\n            : `Default (${core_tz})`}\n          {!disabled && <ChevronsUpDown className=\"w-3 h-3\" />}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[300px] max-h-[300px] p-0 z-[100]\">\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder={\"Search Timezones\"}\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center pt-3 pb-2\">\n              No Timezones Found\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n\n            <CommandGroup>\n              {filtered.map((timezone) =>\n                timezone !== \"Default\" ? (\n                  <CommandItem\n                    key={timezone}\n                    onSelect={() => {\n                      onChange(timezone);\n                      setOpen(false);\n                    }}\n                    className=\"flex items-center justify-between cursor-pointer\"\n                  >\n                    <div className=\"p-1\">\n                      {timezone} ({fmt_utc_offset(timezone)})\n                    </div>\n                  </CommandItem>\n                ) : (\n                  <CommandItem\n                    key={timezone}\n                    onSelect={() => {\n                      onChange(\"\");\n                      setOpen(false);\n                    }}\n                    className=\"flex items-center justify-between cursor-pointer\"\n                  >\n                    <div className=\"p-1\">Default ({core_tz})</div>\n                  </CommandItem>\n                )\n              )}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nexport const TemplateMarker = ({ type }: { type: UsableResource }) => {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <Badge className=\"px-1 py-0\">T</Badge>\n      </TooltipTrigger>\n      <TooltipContent>\n        <div>This {fmt_resource_type(type).toLowerCase()} is a template.</div>\n      </TooltipContent>\n    </Tooltip>\n  );\n};\n\nexport const TemplateQueryBehaviorSelector = () => {\n  const [value, set] = useTemplatesQueryBehavior();\n  return (\n    <Select\n      value={value + \" Templates\"}\n      onValueChange={(value) =>\n        set(value.replace(\" Templates\", \"\") as Types.TemplatesQueryBehavior)\n      }\n    >\n      <SelectTrigger className=\"w-[180px]\">\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {[\n          Types.TemplatesQueryBehavior.Exclude,\n          Types.TemplatesQueryBehavior.Include,\n          Types.TemplatesQueryBehavior.Only,\n        ].map((behavior) => (\n          <SelectItem key={behavior} value={behavior + \" Templates\"}>\n            {behavior} Templates\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n};\n\nexport type ServerAddress = {\n  raw: string;\n  protocol: \"http:\" | \"https:\";\n  hostname: string;\n};\n\nexport const useServerAddress = (\n  server_id: string | undefined\n): ServerAddress | null => {\n  const server = useServer(server_id);\n\n  if (!server) return null;\n  const base = server.info.external_address || server.info.address;\n\n  const parsed = (() => {\n    try {\n      return new URL(base);\n    } catch {\n      return new URL(\"http://\" + base);\n    }\n  })();\n\n  return {\n    raw: base,\n    protocol: parsed.protocol === \"https:\" ? \"https:\" : \"http:\",\n    hostname: parsed.hostname,\n  };\n};\n\nexport const ContainerPortLink = ({\n  host_port,\n  ports,\n  server_id,\n}: {\n  host_port: string;\n  ports: Types.Port[];\n  server_id: string | undefined;\n}) => {\n  const server_address = useServerAddress(server_id);\n\n  if (!server_address) return null;\n\n  const isHttps = server_address.protocol === \"https:\";\n  const link = host_port === \"443\" && isHttps\n    ? `https://${server_address.hostname}`\n    : `http://${server_address.hostname}:${host_port}`;\n\n  const uniqueHostPorts = Array.from(\n    new Set(\n      ports\n        .map((p) => p.PublicPort)\n        .filter((p): p is number => typeof p === \"number\")\n        .map((n) => Number(n))\n        .filter((n) => !Number.isNaN(n))\n    )\n  ).sort((a, b) => a - b);\n  const display_text =\n    uniqueHostPorts.length <= 1\n      ? String(uniqueHostPorts[0] ?? host_port)\n      : `${uniqueHostPorts[0]}-${uniqueHostPorts[uniqueHostPorts.length - 1]}`;\n\n  return (\n    <Tooltip>\n      <TooltipTrigger>\n        <a\n          target=\"_blank\"\n          href={link}\n          className=\"text-sm cursor-pointer hover:underline px-1 py-2 flex items-center gap-2\"\n        >\n          <EthernetPort\n            className={cn(\"w-4 h-4\", stroke_color_class_by_intention(\"Good\"))}\n          />\n          {display_text}\n        </a>\n      </TooltipTrigger>\n      <TooltipContent className=\"flex flex-col gap-2 w-fit\">\n        <a\n          target=\"_blank\"\n          href={link}\n          className=\"text-sm cursor-pointer hover:underline flex items-center gap-2\"\n        >\n          <LinkIcon className=\"w-3 h-3\" />\n          {link}\n        </a>\n        {ports.slice(0, 10).map((port, i) => (\n          <div key={i} className=\"flex gap-2 text-sm text-muted-foreground\">\n            <span>-</span>\n            <div>{fmt_port_mount(port)}</div>\n          </div>\n        ))}\n        {ports.length > 10 && (\n          <div className=\"flex gap-2 text-sm text-muted-foreground\">\n            <span>+</span>\n            <div>{ports.length - 10} more…</div>\n          </div>\n        )}\n      </TooltipContent>\n    </Tooltip>\n  );\n};\n\nexport const ContainerPortsTableView = ({\n  ports,\n  server_id,\n}: {\n  ports: Types.Port[];\n  server_id: string | undefined;\n}) => {\n  const portsMap = useContainerPortsMap(ports);\n  const sortedNumericPorts = Object.keys(portsMap)\n    .map(Number)\n    .filter((port) => !Number.isNaN(port))\n    .sort((a, b) => a - b);\n\n  type Group = { start: number; end: number; ports: Types.Port[] };\n\n  const groupedPorts = sortedNumericPorts.reduce<Group[]>((acc, port) => {\n    const lastGroup = acc[acc.length - 1];\n    const currentPorts = portsMap[String(port)] || [];\n    if (lastGroup && port === lastGroup.end + 1) {\n      lastGroup.end = port;\n      lastGroup.ports.push(...currentPorts);\n    } else {\n      acc.push({ start: port, end: port, ports: currentPorts });\n    }\n    return acc;\n  }, []);\n\n  return (\n    <div className=\"flex items-center gap-x-1 flex-wrap\">\n      {groupedPorts.map((group, i) => (\n        <Fragment key={group.start}>\n          {i > 0 && <span className=\"text-muted-foreground\">|</span>}\n          <ContainerPortLink\n            host_port={String(group.start)}\n            ports={group.ports}\n            server_id={server_id}\n          />\n        </Fragment>\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  * {\n    @apply border-border scroll-smooth;\n  }\n  *::-webkit-scrollbar {\n    @apply w-1 h-1 bg-background;\n  }\n  *::-webkit-scrollbar-thumb {\n    @apply rounded-lg bg-primary;\n  }\n\n  body {\n    @apply bg-background text-foreground;\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\",\n      \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\",\n      \"Helvetica Neue\", sans-serif;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    overflow-y: hidden;\n  }\n  pre {\n    @apply bg-card text-card-foreground border rounded-xl min-h-full text-xs p-4 whitespace-pre-wrap scroll-m-4 break-all;\n  }\n  code {\n    font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\",\n      monospace;\n  }\n}\n\n@layer base {\n  :root {\n    --background: 220.1 0.1% 98.3%;\n    --foreground: 220.1 40.1% 5.2%;\n    --card: 0 0% 100%;\n    --card-foreground: 224 71.4% 4.1%;\n    --popover: 0 0% 100%;\n    --popover-foreground: 224 71.4% 4.1%;\n    --primary: 220.9 39.3% 11%;\n    --primary-foreground: 210 20% 98%;\n    --secondary: 220 14.3% 95.9%;\n    --secondary-foreground: 220.9 39.3% 11%;\n    --muted: 220 14.3% 95.9%;\n    --muted-foreground: 220 8.9% 46.1%;\n    --accent: 220 14.3% 95.9%;\n    --accent-foreground: 220.9 39.3% 11%;\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 20% 98%;\n    --border: 220 13% 91%;\n    --input: 220 13% 91%;\n    --ring: 224 71.4% 4.1%;\n    --radius: 0.5rem;\n    --chart-1: 12 76% 61%;\n    --chart-2: 173 58% 39%;\n    --chart-3: 197 37% 24%;\n    --chart-4: 43 74% 66%;\n    --chart-5: 27 87% 67%;\n  }\n\n  .dark {\n    --background: 220.1 27.9% 6.1%;\n    --foreground: 210 20% 98%;\n    --card: 220.1 27.9% 6.1%;\n    --card-foreground: 210 20% 98%;\n    --popover: 220.1 27.9% 6.1%;\n    --popover-foreground: 210 20% 98%;\n    --primary: 210 20% 98%;\n    --primary-foreground: 220.9 39.3% 11%;\n    --secondary: 220.1 27.9% 12.9%;\n    --secondary-foreground: 210 20% 98%;\n    --muted: 220.1 27.9% 12.9%;\n    --muted-foreground: 217.9 10.6% 64.9%;\n    --accent: 220.1 27.9% 12.9%;\n    --accent-foreground: 210 20% 98%;\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 20% 98%;\n    --border: 220.1 27.9% 12.9%;\n    --input: 220.1 27.9% 12.9%;\n    --ring: 216 12.2% 83.9%;\n    --chart-1: 220 70% 50%;\n    --chart-2: 160 60% 45%;\n    --chart-3: 30 80% 55%;\n    --chart-4: 280 65% 60%;\n    --chart-5: 340 75% 55%;\n  }\n}\n\n.monaco-editor {\n  position: absolute !important;\n}\n\n.xterm {\n  padding: 12px !important;\n}\n\n.xterm .xterm-viewport::-webkit-scrollbar {\n  @apply w-[16px] rounded-sm\n}"
  },
  {
    "path": "frontend/src/lib/color.ts",
    "content": "import { Types } from \"komodo_client\";\n\nexport type ColorIntention =\n  | \"Good\"\n  | \"Neutral\"\n  | \"Warning\"\n  | \"Critical\"\n  | \"Unknown\"\n  | \"None\";\n\nexport const hex_color_by_intention = (intention: ColorIntention) => {\n  switch (intention) {\n    case \"Good\":\n      return \"#22C55E\";\n    case \"Neutral\":\n      return \"#3B82F6\";\n    case \"Warning\":\n      return \"#EAB308\";\n    case \"Critical\":\n      return \"#EF0044\";\n    case \"Unknown\":\n      return \"#A855F7\";\n    case \"None\":\n      return \"\";\n  }\n};\n\nexport const fill_color_class_by_intention = (intention: ColorIntention) => {\n  switch (intention) {\n    case \"Good\":\n      return \"text-green-400 dark:text-green-700\";\n    case \"Neutral\":\n      return \"text-blue-400 dark:text-blue-700\";\n    case \"Warning\":\n      return \"text-yellow-500 dark:text-yellow-400\";\n    case \"Critical\":\n      return \"text-red-400 dark:text-red-700\";\n    case \"Unknown\":\n      return \"text-purple-400 dark:text-purple-700\";\n    case \"None\":\n      return \"\";\n  }\n};\n\nexport const stroke_color_class_by_intention = (intention: ColorIntention) => {\n  switch (intention) {\n    case \"Good\":\n      return \"stroke-green-600 dark:stroke-green-500\";\n    case \"Neutral\":\n      return \"stroke-blue-600 dark:stroke-blue-500\";\n    case \"Warning\":\n      return \"stroke-yellow-500 dark:stroke-yellow-400\";\n    case \"Critical\":\n      return \"stroke-red-600 dark:stroke-red-500\";\n    case \"Unknown\":\n      return \"stroke-purple-600 dark:stroke-purple-500\";\n    case \"None\":\n      return \"\";\n  }\n};\n\nexport const bg_color_class_by_intention = (intention: ColorIntention) => {\n  switch (intention) {\n    case \"Good\":\n      return \"bg-green-400 dark:bg-green-700\";\n    case \"Neutral\":\n      return \"bg-blue-400 dark:bg-blue-700\";\n    case \"Warning\":\n      return \"bg-yellow-500 dark:bg-yellow-600\";\n    case \"Critical\":\n      return \"bg-red-400 dark:bg-red-700\";\n    case \"Unknown\":\n      return \"bg-purple-400 dark:bg-purple-700\";\n    case \"None\":\n      return \"\";\n  }\n};\n\nexport const border_color_class_by_intention = (intention: ColorIntention) => {\n  switch (intention) {\n    case \"Good\":\n      return \"border-green-700 dark:border-green-400\";\n    case \"Neutral\":\n      return \"border-blue-700 dark:border-blue-400\";\n    case \"Warning\":\n      return \"border-yellow-600 dark:border-yellow-400\";\n    case \"Critical\":\n      return \"border-red-700 dark:border-red-400\";\n    case \"Unknown\":\n      return \"border-purple-700 dark:border-purple-400\";\n    case \"None\":\n      return \"\";\n  }\n};\n\nexport const text_color_class_by_intention = (intention: ColorIntention) => {\n  switch (intention) {\n    case \"Good\":\n      return \"text-green-700 dark:text-green-400\";\n    case \"Neutral\":\n      return \"text-blue-700 dark:text-blue-400\";\n    case \"Warning\":\n      return \"text-yellow-600 dark:text-yellow-400\";\n    case \"Critical\":\n      return \"text-red-700 dark:text-red-400\";\n    case \"Unknown\":\n      return \"text-purple-700 dark:text-purple-400\";\n    case \"None\":\n      return \"\";\n  }\n};\n\nexport const soft_text_color_class_by_intention = (\n  intention: ColorIntention\n) => {\n  switch (intention) {\n    case \"Good\":\n      return \"text-green-700/60 dark:text-green-400/60\";\n    case \"Neutral\":\n      return \"text-blue-700/60 dark:text-blue-400/60\";\n    case \"Warning\":\n      return \"text-yellow-600/60 dark:text-yellow-400/60\";\n    case \"Critical\":\n      return \"text-red-700/60 dark:text-red-400/60\";\n    case \"Unknown\":\n      return \"text-purple-700/60 dark:text-purple-400/60\";\n    case \"None\":\n      return \"\";\n  }\n};\n\nexport const server_state_intention: (\n  state?: Types.ServerState,\n  hasVersionMismatch?: boolean\n) => ColorIntention = (state, hasVersionMismatch) => {\n  switch (state) {\n    case Types.ServerState.Ok:\n      // If there's a version mismatch and the server is \"Ok\", show warning instead\n      return hasVersionMismatch ? \"Warning\" : \"Good\";\n    case Types.ServerState.NotOk:\n      return \"Critical\";\n    case Types.ServerState.Disabled:\n      return \"Neutral\";\n    case undefined:\n      return \"None\";\n  }\n};\n\nexport const deployment_state_intention: (\n  state?: Types.DeploymentState\n) => ColorIntention = (state) => {\n  switch (state) {\n    case undefined:\n      return \"None\";\n    case Types.DeploymentState.Deploying:\n      return \"Warning\";\n    case Types.DeploymentState.Running:\n      return \"Good\";\n    case Types.DeploymentState.NotDeployed:\n      return \"Neutral\";\n    case Types.DeploymentState.Paused:\n      return \"Warning\";\n    case Types.DeploymentState.Unknown:\n      return \"Unknown\";\n    default:\n      return \"Critical\";\n  }\n};\n\nexport const container_state_intention: (\n  state?: Types.ContainerStateStatusEnum\n) => ColorIntention = (state) => {\n  switch (state) {\n    case undefined:\n      return \"None\";\n    case Types.ContainerStateStatusEnum.Running:\n      return \"Good\";\n    case Types.ContainerStateStatusEnum.Paused:\n      return \"Warning\";\n    case Types.ContainerStateStatusEnum.Empty:\n      return \"Unknown\";\n    default:\n      return \"Critical\";\n  }\n};\n\nexport const build_state_intention = (status?: Types.BuildState) => {\n  switch (status) {\n    case undefined:\n      return \"None\";\n    case Types.BuildState.Unknown:\n      return \"Unknown\";\n    case Types.BuildState.Ok:\n      return \"Good\";\n    case Types.BuildState.Building:\n      return \"Warning\";\n    case Types.BuildState.Failed:\n      return \"Critical\";\n    default:\n      return \"None\";\n  }\n};\n\nexport const repo_state_intention = (state?: Types.RepoState) => {\n  switch (state) {\n    case undefined:\n      return \"None\";\n    case Types.RepoState.Unknown:\n      return \"Unknown\";\n    case Types.RepoState.Ok:\n      return \"Good\";\n    case Types.RepoState.Cloning:\n      return \"Warning\";\n    case Types.RepoState.Pulling:\n      return \"Warning\";\n    case Types.RepoState.Building:\n      return \"Warning\";\n    case Types.RepoState.Failed:\n      return \"Critical\";\n    default:\n      return \"None\";\n  }\n};\n\nexport const stack_state_intention = (state?: Types.StackState) => {\n  switch (state) {\n    case undefined:\n      return \"None\";\n    case Types.StackState.Deploying:\n      return \"Warning\";\n    case Types.StackState.Running:\n      return \"Good\";\n    case Types.StackState.Paused:\n      return \"Warning\";\n    case Types.StackState.Stopped:\n      return \"Critical\";\n    case Types.StackState.Restarting:\n      return \"Critical\";\n    case Types.StackState.Down:\n      return \"Neutral\";\n    case Types.StackState.Unknown:\n      return \"Unknown\";\n    default:\n      return \"Critical\";\n  }\n};\n\nexport const procedure_state_intention = (status?: Types.ProcedureState) => {\n  switch (status) {\n    case undefined:\n      return \"None\";\n    case Types.ProcedureState.Unknown:\n      return \"Unknown\";\n    case Types.ProcedureState.Ok:\n      return \"Good\";\n    case Types.ProcedureState.Running:\n      return \"Warning\";\n    case Types.ProcedureState.Failed:\n      return \"Critical\";\n    default:\n      return \"None\";\n  }\n};\n\nexport const action_state_intention = (status?: Types.ActionState) => {\n  switch (status) {\n    case undefined:\n      return \"None\";\n    case Types.ActionState.Unknown:\n      return \"Unknown\";\n    case Types.ActionState.Ok:\n      return \"Good\";\n    case Types.ActionState.Running:\n      return \"Warning\";\n    case Types.ActionState.Failed:\n      return \"Critical\";\n    default:\n      return \"None\";\n  }\n};\n\nexport const resource_sync_state_intention = (\n  status?: Types.ResourceSyncState\n) => {\n  switch (status) {\n    case undefined:\n      return \"None\";\n    case Types.ResourceSyncState.Unknown:\n      return \"Unknown\";\n    case Types.ResourceSyncState.Ok:\n      return \"Good\";\n    case Types.ResourceSyncState.Syncing:\n      return \"Warning\";\n    case Types.ResourceSyncState.Pending:\n      return \"Warning\";\n    case Types.ResourceSyncState.Failed:\n      return \"Critical\";\n    default:\n      return \"None\";\n  }\n};\n\nexport const alert_level_intention: (\n  level: Types.SeverityLevel\n) => ColorIntention = (level) => {\n  switch (level) {\n    case Types.SeverityLevel.Ok:\n      return \"Good\";\n    case Types.SeverityLevel.Warning:\n      return \"Warning\";\n    case Types.SeverityLevel.Critical:\n      return \"Critical\";\n  }\n};\n\nexport const diff_type_intention: (\n  level: Types.DiffData[\"type\"],\n  reverse: boolean\n) => ColorIntention = (level, reverse) => {\n  switch (level) {\n    case \"Create\":\n      return reverse ? \"Critical\" : \"Good\";\n    case \"Update\":\n      return \"Neutral\";\n    case \"Delete\":\n      return reverse ? \"Good\" : \"Critical\";\n  }\n};\n\nexport const tag_background_class = (color?: Types.TagColor) => {\n  return `bg-${tag_color(color)}`;\n};\n\nexport const tag_color = (color?: Types.TagColor) => {\n  switch (color) {\n    case undefined:\n      return \"slate-600\";\n    case Types.TagColor.LightSlate:\n      return \"slate-400\";\n    case Types.TagColor.Slate:\n      return \"slate-600\";\n    case Types.TagColor.DarkSlate:\n      return \"slate-900\";\n\n    case Types.TagColor.LightRed:\n      return \"red-400\";\n    case Types.TagColor.Red:\n      return \"red-600\";\n    case Types.TagColor.DarkRed:\n      return \"red-900\";\n\n    case Types.TagColor.LightOrange:\n      return \"orange-400\";\n    case Types.TagColor.Orange:\n      return \"orange-600\";\n    case Types.TagColor.DarkOrange:\n      return \"orange-900\";\n\n    case Types.TagColor.LightAmber:\n      return \"amber-400\";\n    case Types.TagColor.Amber:\n      return \"amber-600\";\n    case Types.TagColor.DarkAmber:\n      return \"amber-900\";\n\n    case Types.TagColor.LightYellow:\n      return \"yellow-400\";\n    case Types.TagColor.Yellow:\n      return \"yellow-600\";\n    case Types.TagColor.DarkYellow:\n      return \"yellow-900\";\n\n    case Types.TagColor.LightLime:\n      return \"lime-400\";\n    case Types.TagColor.Lime:\n      return \"lime-600\";\n    case Types.TagColor.DarkLime:\n      return \"lime-900\";\n\n    case Types.TagColor.LightGreen:\n      return \"green-400\";\n    case Types.TagColor.Green:\n      return \"green-600\";\n    case Types.TagColor.DarkGreen:\n      return \"green-900\";\n\n    case Types.TagColor.LightEmerald:\n      return \"emerald-400\";\n    case Types.TagColor.Emerald:\n      return \"emerald-600\";\n    case Types.TagColor.DarkEmerald:\n      return \"emerald-900\";\n\n    case Types.TagColor.LightTeal:\n      return \"teal-400\";\n    case Types.TagColor.Teal:\n      return \"teal-600\";\n    case Types.TagColor.DarkTeal:\n      return \"teal-900\";\n\n    case Types.TagColor.LightCyan:\n      return \"cyan-400\";\n    case Types.TagColor.Cyan:\n      return \"cyan-600\";\n    case Types.TagColor.DarkCyan:\n      return \"cyan-900\";\n\n    case Types.TagColor.LightSky:\n      return \"sky-400\";\n    case Types.TagColor.Sky:\n      return \"sky-600\";\n    case Types.TagColor.DarkSky:\n      return \"sky-900\";\n\n    case Types.TagColor.LightBlue:\n      return \"blue-400\";\n    case Types.TagColor.Blue:\n      return \"blue-600\";\n    case Types.TagColor.DarkBlue:\n      return \"blue-900\";\n\n    case Types.TagColor.LightIndigo:\n      return \"indigo-400\";\n    case Types.TagColor.Indigo:\n      return \"indigo-600\";\n    case Types.TagColor.DarkIndigo:\n      return \"indigo-900\";\n\n    case Types.TagColor.LightViolet:\n      return \"violet-400\";\n    case Types.TagColor.Violet:\n      return \"violet-600\";\n    case Types.TagColor.DarkViolet:\n      return \"violet-900\";\n\n    case Types.TagColor.LightPurple:\n      return \"purple-400\";\n    case Types.TagColor.Purple:\n      return \"purple-600\";\n    case Types.TagColor.DarkPurple:\n      return \"purple-900\";\n\n    case Types.TagColor.LightFuchsia:\n      return \"fuchsia-400\";\n    case Types.TagColor.Fuchsia:\n      return \"fuchsia-600\";\n    case Types.TagColor.DarkFuchsia:\n      return \"fuchsia-900\";\n\n    case Types.TagColor.LightPink:\n      return \"pink-400\";\n    case Types.TagColor.Pink:\n      return \"pink-600\";\n    case Types.TagColor.DarkPink:\n      return \"pink-900\";\n\n    case Types.TagColor.LightRose:\n      return \"rose-400\";\n    case Types.TagColor.Rose:\n      return \"rose-600\";\n    case Types.TagColor.DarkRose:\n      return \"rose-900\";\n  }\n};\n"
  },
  {
    "path": "frontend/src/lib/dashboard-preferences.ts",
    "content": "import { atomWithStorage } from \"@lib/hooks\";\nimport { useAtom } from \"jotai\";\n\ninterface DashboardPreferences {\n  showServerStats: boolean;\n}\n\nconst DEFAULT_PREFERENCES: DashboardPreferences = {\n  showServerStats: false,\n};\n\nexport const dashboardPreferencesAtom = atomWithStorage<DashboardPreferences>(\n  \"komodo-dashboard-preferences\",\n  DEFAULT_PREFERENCES\n);\n\nexport const useDashboardPreferences = () => {\n  const [preferences, setPreferences] = useAtom(dashboardPreferencesAtom);\n\n  const updatePreference = <K extends keyof DashboardPreferences>(\n    key: K,\n    value: DashboardPreferences[K]\n  ) => {\n    setPreferences({ ...preferences, [key]: value });\n  };\n\n  return {\n    preferences,\n    updatePreference,\n  };\n};\n"
  },
  {
    "path": "frontend/src/lib/formatting.ts",
    "content": "import { UsableResource } from \"@types\";\nimport { Types } from \"komodo_client\";\n\nexport const fmt_date = (d: Date) => {\n  const hours = d.getHours();\n  const minutes = d.getMinutes();\n  return `${fmt_month(d.getMonth())} ${d.getDate()} ${\n    hours > 9 ? hours : \"0\" + hours\n  }:${minutes > 9 ? minutes : \"0\" + minutes}`;\n};\n\nexport const fmt_utc_date = (d: Date) => {\n  const hours = d.getUTCHours();\n  const minutes = d.getUTCMinutes();\n  return `${fmt_month(d.getUTCMonth())} ${d.getUTCDate()} ${\n    hours > 9 ? hours : \"0\" + hours\n  }:${minutes > 9 ? minutes : \"0\" + minutes}`;\n};\n\nconst fmt_month = (month: number) => {\n  switch (month) {\n    case 0:\n      return \"Jan\";\n    case 1:\n      return \"Feb\";\n    case 2:\n      return \"Mar\";\n    case 3:\n      return \"Apr\";\n    case 4:\n      return \"May\";\n    case 5:\n      return \"Jun\";\n    case 6:\n      return \"Jul\";\n    case 7:\n      return \"Aug\";\n    case 8:\n      return \"Sep\";\n    case 9:\n      return \"Oct\";\n    case 10:\n      return \"Nov\";\n    case 11:\n      return \"Dec\";\n  }\n};\n\nexport const fmt_date_with_minutes = (d: Date) => {\n  // return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`;\n  return d.toLocaleString();\n};\n\nexport const fmt_version = (version: Types.Version | undefined) => {\n  if (!version) return \"...\";\n  const { major, minor, patch } = version;\n  if (major === 0 && minor === 0 && patch === 0) return \"Latest\";\n  return `v${major}.${minor}.${patch}`;\n};\n\nexport const fmt_duration = (start_ts: number, end_ts: number) => {\n  const start = new Date(start_ts);\n  const end = new Date(end_ts);\n  const durr = end.getTime() - start.getTime();\n  const seconds = durr / 1000;\n  const minutes = Math.floor(seconds / 60);\n  const remaining_seconds = seconds % 60;\n  return `${\n    minutes > 0 ? `${minutes} minute${minutes > 1 ? \"s\" : \"\"} ` : \"\"\n  }${remaining_seconds.toFixed(minutes > 0 ? 0 : 1)} seconds`;\n};\n\nexport const fmt_operation = (operation: Types.Operation) => {\n  return operation.match(/[A-Z][a-z]+|[0-9]+/g)?.join(\" \")!;\n};\n\nexport const fmt_upper_camelcase = (input: string) => {\n  return input.match(/[A-Z][a-z]+|[0-9]+/g)?.join(\" \")!;\n};\n\n/// list_all_items => List All Items\nexport function snake_case_to_upper_space_case(snake: string) {\n  if (snake.length === 0) return \"\";\n  return snake\n    .split(\"_\")\n    .map((item) => item[0].toUpperCase() + item.slice(1))\n    .join(\" \");\n}\n\nconst BYTES_PER_MB = 1e6;\nconst BYTES_PER_GB = BYTES_PER_MB * 1000;\n\nexport function format_size_bytes(size_bytes: number) {\n  if (size_bytes > BYTES_PER_GB) {\n    return `${(size_bytes / BYTES_PER_GB).toFixed(1)} GB`;\n  } else {\n    return `${(size_bytes / BYTES_PER_MB).toFixed(1)} MB`;\n  }\n}\n\nexport function fmt_resource_type(type: UsableResource) {\n  if (type === \"ResourceSync\") {\n    return \"Resource Sync\";\n  }\n  return type;\n}\n\nexport function fmt_utc_offset(tz: Types.IanaTimezone): string {\n  switch (tz) {\n    case Types.IanaTimezone.EtcGmtMinus12:\n      return \"UTC-12:00\";\n    case Types.IanaTimezone.PacificPagoPago:\n      return \"UTC-11:00\";\n    case Types.IanaTimezone.PacificHonolulu:\n      return \"UTC-10:00\";\n    case Types.IanaTimezone.PacificMarquesas:\n      return \"UTC-09:30\";\n    case Types.IanaTimezone.AmericaAnchorage:\n      return \"UTC-09:00\";\n    case Types.IanaTimezone.AmericaLosAngeles:\n      return \"UTC-08:00\";\n    case Types.IanaTimezone.AmericaDenver:\n      return \"UTC-07:00\";\n    case Types.IanaTimezone.AmericaChicago:\n      return \"UTC-06:00\";\n    case Types.IanaTimezone.AmericaNewYork:\n      return \"UTC-05:00\";\n    case Types.IanaTimezone.AmericaHalifax:\n      return \"UTC-04:00\";\n    case Types.IanaTimezone.AmericaStJohns:\n      return \"UTC-03:30\";\n    case Types.IanaTimezone.AmericaSaoPaulo:\n      return \"UTC-03:00\";\n    case Types.IanaTimezone.AmericaNoronha:\n      return \"UTC-02:00\";\n    case Types.IanaTimezone.AtlanticAzores:\n      return \"UTC-01:00\";\n    case Types.IanaTimezone.EtcUtc:\n      return \"UTC+00:00\";\n    case Types.IanaTimezone.EuropeBerlin:\n      return \"UTC+01:00\";\n    case Types.IanaTimezone.EuropeBucharest:\n      return \"UTC+02:00\";\n    case Types.IanaTimezone.EuropeMoscow:\n      return \"UTC+03:00\";\n    case Types.IanaTimezone.AsiaTehran:\n      return \"UTC+03:30\";\n    case Types.IanaTimezone.AsiaDubai:\n      return \"UTC+04:00\";\n    case Types.IanaTimezone.AsiaKabul:\n      return \"UTC+04:30\";\n    case Types.IanaTimezone.AsiaKarachi:\n      return \"UTC+05:00\";\n    case Types.IanaTimezone.AsiaKolkata:\n      return \"UTC+05:30\";\n    case Types.IanaTimezone.AsiaKathmandu:\n      return \"UTC+05:45\";\n    case Types.IanaTimezone.AsiaDhaka:\n      return \"UTC+06:00\";\n    case Types.IanaTimezone.AsiaYangon:\n      return \"UTC+06:30\";\n    case Types.IanaTimezone.AsiaBangkok:\n      return \"UTC+07:00\";\n    case Types.IanaTimezone.AsiaShanghai:\n      return \"UTC+08:00\";\n    case Types.IanaTimezone.AustraliaEucla:\n      return \"UTC+08:45\";\n    case Types.IanaTimezone.AsiaTokyo:\n      return \"UTC+09:00\";\n    case Types.IanaTimezone.AustraliaAdelaide:\n      return \"UTC+09:30\";\n    case Types.IanaTimezone.AustraliaSydney:\n      return \"UTC+10:00\";\n    case Types.IanaTimezone.AustraliaLordHowe:\n      return \"UTC+10:30\";\n    case Types.IanaTimezone.PacificPortMoresby:\n      return \"UTC+11:00\";\n    case Types.IanaTimezone.PacificAuckland:\n      return \"UTC+12:00\";\n    case Types.IanaTimezone.PacificChatham:\n      return \"UTC+12:45\";\n    case Types.IanaTimezone.PacificTongatapu:\n      return \"UTC+13:00\";\n    case Types.IanaTimezone.PacificKiritimati:\n      return \"UTC+14:00\";\n  }\n}\n\nexport function fmt_port_mount(port: Types.Port) {\n  return `${port.IP ? port.IP + \":\" : \"\"}${port.PublicPort ?? \"NONE\"}:${port.PrivatePort ?? \"NONE\"}${port.Type ? \"/\" + port.Type : \"\"}`;\n}\n"
  },
  {
    "path": "frontend/src/lib/hooks.ts",
    "content": "import { KOMODO_BASE_URL } from \"@main\";\nimport { KomodoClient, Types } from \"komodo_client\";\nimport {\n  AuthResponses,\n  ExecuteResponses,\n  ReadResponses,\n  UserResponses,\n  WriteResponses,\n} from \"komodo_client/dist/responses\";\nimport {\n  UseMutationOptions,\n  UseQueryOptions,\n  useMutation,\n  useQuery,\n  useQueryClient,\n} from \"@tanstack/react-query\";\nimport { UsableResource } from \"@types\";\nimport { useToast } from \"@ui/use-toast\";\nimport { atom, useAtom } from \"jotai\";\nimport { atomFamily } from \"jotai/utils\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport { has_minimum_permissions, RESOURCE_TARGETS } from \"./utils\";\n\nexport const atomWithStorage = <T>(key: string, init: T) => {\n  const stored = localStorage.getItem(key);\n  const inner = atom(stored ? JSON.parse(stored) : init);\n\n  return atom(\n    (get) => get(inner),\n    (_, set, newValue) => {\n      set(inner, newValue);\n      localStorage.setItem(key, JSON.stringify(newValue));\n    }\n  );\n};\n\ntype LoginTokens = {\n  /** Current User ID */\n  current: string | undefined;\n  /** Array of logged in user ids / tokens */\n  tokens: Array<Types.JwtResponse>;\n};\n\nconst LOGIN_TOKENS_KEY = \"komodo-auth-tokens-v1\";\n\nexport const LOGIN_TOKENS = (() => {\n  const stored = localStorage.getItem(LOGIN_TOKENS_KEY);\n\n  let tokens: LoginTokens = stored\n    ? JSON.parse(stored)\n    : { current: undefined, tokens: [] };\n\n  const update_local_storage = () => {\n    localStorage.setItem(LOGIN_TOKENS_KEY, JSON.stringify(tokens));\n  };\n\n  const accounts = () => {\n    const current = tokens.tokens.find((t) => t.user_id === tokens.current);\n    const filtered = tokens.tokens.filter((t) => t.user_id !== tokens.current);\n    return current ? [current, ...filtered] : filtered;\n  };\n\n  const add_and_change = (user_id: string, jwt: string) => {\n    const filtered = tokens.tokens.filter((t) => t.user_id !== user_id);\n    filtered.push({ user_id, jwt });\n    filtered.sort();\n    tokens = {\n      current: user_id,\n      tokens: filtered,\n    };\n    update_local_storage();\n  };\n\n  const remove = (user_id: string) => {\n    const filtered = tokens.tokens.filter((t) => t.user_id !== user_id);\n    tokens = {\n      current:\n        tokens.current === user_id ? filtered[0]?.user_id : tokens.current,\n      tokens: filtered,\n    };\n    update_local_storage();\n  };\n\n  const remove_all = () => {\n    tokens = {\n      current: undefined,\n      tokens: [],\n    };\n    update_local_storage();\n  };\n\n  const change = (to_id: string) => {\n    tokens = {\n      current: to_id,\n      tokens: tokens.tokens,\n    };\n    update_local_storage();\n  };\n\n  return {\n    jwt: () =>\n      tokens.current\n        ? (tokens.tokens.find((t) => t.user_id === tokens.current)?.jwt ?? \"\")\n        : \"\",\n    accounts,\n    add_and_change,\n    remove,\n    remove_all,\n    change,\n  };\n})();\n\nexport const komodo_client = () =>\n  KomodoClient(KOMODO_BASE_URL, {\n    type: \"jwt\",\n    params: { jwt: LOGIN_TOKENS.jwt() },\n  });\n\n// ============== RESOLVER ==============\n\nexport const useLoginOptions = () => {\n  return useQuery({\n    queryKey: [\"GetLoginOptions\"],\n    queryFn: () => komodo_client().auth(\"GetLoginOptions\", {}),\n  });\n};\n\nexport const useUser = () => {\n  const userReset = useUserReset();\n  const hasJwt = !!LOGIN_TOKENS.jwt();\n  \n  const query = useQuery({\n    queryKey: [\"GetUser\"],\n    queryFn: () => komodo_client().auth(\"GetUser\", {}),\n    refetchInterval: 30_000,\n    enabled: hasJwt,\n  });\n  \n  useEffect(() => {\n    if (query.data && query.error) {\n      userReset();\n    }\n  }, [query.data, query.error]);\n  \n  return query;\n};\n\nexport const useUserInvalidate = () => {\n  const qc = useQueryClient();\n  return () => {\n    qc.invalidateQueries({ queryKey: [\"GetUser\"] });\n  };\n};\n\nexport const useUserReset = () => {\n  const qc = useQueryClient();\n  return () => {\n    qc.resetQueries({ queryKey: [\"GetUser\"] });\n  };\n};\n\nexport const useRead = <\n  T extends Types.ReadRequest[\"type\"],\n  R extends Extract<Types.ReadRequest, { type: T }>,\n  P extends R[\"params\"],\n  C extends Omit<\n    UseQueryOptions<\n      ReadResponses[R[\"type\"]],\n      unknown,\n      ReadResponses[R[\"type\"]],\n      (T | P)[]\n    >,\n    \"queryFn\" | \"queryKey\"\n  >,\n>(\n  type: T,\n  params: P,\n  config?: C\n) => {\n  const hasJwt = !!LOGIN_TOKENS.jwt();\n  return useQuery({\n    queryKey: [type, params],\n    queryFn: () => komodo_client().read<T, R>(type, params),\n    enabled: hasJwt && (config?.enabled !== false),\n    ...config,\n  });\n};\n\nexport const useInvalidate = () => {\n  const qc = useQueryClient();\n  return <\n    Type extends Types.ReadRequest[\"type\"],\n    Params extends Extract<Types.ReadRequest, { type: Type }>[\"params\"],\n  >(\n    ...keys: Array<[Type] | [Type, Params]>\n  ) => keys.forEach((key) => qc.invalidateQueries({ queryKey: key }));\n};\n\nexport const useManageUser = <\n  T extends Types.UserRequest[\"type\"],\n  R extends Extract<Types.UserRequest, { type: T }>,\n  P extends R[\"params\"],\n  C extends Omit<\n    UseMutationOptions<UserResponses[T], unknown, P, unknown>,\n    \"mutationKey\" | \"mutationFn\"\n  >,\n>(\n  type: T,\n  config?: C\n) => {\n  const { toast } = useToast();\n  return useMutation({\n    mutationKey: [type],\n    mutationFn: (params: P) => komodo_client().user<T, R>(type, params),\n    onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {\n      console.log(\"Auth error:\", e);\n      const msg = e.result?.error ?? \"Unknown error. See console.\";\n      const detail = e.result?.trace\n        ?.map((msg) => msg[0].toUpperCase() + msg.slice(1))\n        .join(\" | \");\n      let msg_log = msg ? msg[0].toUpperCase() + msg.slice(1) + \" | \" : \"\";\n      if (detail) {\n        msg_log += detail + \" | \";\n      }\n      toast({\n        title: `Request ${type} Failed`,\n        description: `${msg_log}See console for details`,\n        variant: \"destructive\",\n      });\n      config?.onError && config.onError(e, v, c);\n    },\n    ...config,\n  });\n};\n\nexport const useWrite = <\n  T extends Types.WriteRequest[\"type\"],\n  R extends Extract<Types.WriteRequest, { type: T }>,\n  P extends R[\"params\"],\n  C extends Omit<\n    UseMutationOptions<WriteResponses[R[\"type\"]], unknown, P, unknown>,\n    \"mutationKey\" | \"mutationFn\"\n  >,\n>(\n  type: T,\n  config?: C\n) => {\n  const { toast } = useToast();\n  return useMutation({\n    mutationKey: [type],\n    mutationFn: (params: P) => komodo_client().write<T, R>(type, params),\n    onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {\n      console.log(\"Write error:\", e);\n      const msg = e.result.error ?? \"Unknown error. See console.\";\n      const detail = e.result?.trace\n        ?.map((msg) => msg[0].toUpperCase() + msg.slice(1))\n        .join(\" | \");\n      let msg_log = msg ? msg[0].toUpperCase() + msg.slice(1) + \" | \" : \"\";\n      if (detail) {\n        msg_log += detail + \" | \";\n      }\n      toast({\n        title: `Write request ${type} failed`,\n        description: `${msg_log}See console for details`,\n        variant: \"destructive\",\n      });\n      config?.onError && config.onError(e, v, c);\n    },\n    ...config,\n  });\n};\n\nexport const useExecute = <\n  T extends Types.ExecuteRequest[\"type\"],\n  R extends Extract<Types.ExecuteRequest, { type: T }>,\n  P extends R[\"params\"],\n  C extends Omit<\n    UseMutationOptions<ExecuteResponses[T], unknown, P, unknown>,\n    \"mutationKey\" | \"mutationFn\"\n  >,\n>(\n  type: T,\n  config?: C\n) => {\n  const { toast } = useToast();\n  return useMutation({\n    mutationKey: [type],\n    mutationFn: (params: P) => komodo_client().execute<T, R>(type, params),\n    onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {\n      console.log(\"Execute error:\", e);\n      const msg = e.result.error ?? \"Unknown error. See console.\";\n      const detail = e.result?.trace\n        ?.map((msg) => msg[0].toUpperCase() + msg.slice(1))\n        .join(\" | \");\n      let msg_log = msg ? msg[0].toUpperCase() + msg.slice(1) + \" | \" : \"\";\n      if (detail) {\n        msg_log += detail + \" | \";\n      }\n      toast({\n        title: `Execute request ${type} failed`,\n        description: `${msg_log}See console for details`,\n        variant: \"destructive\",\n      });\n      config?.onError && config.onError(e, v, c);\n    },\n    ...config,\n  });\n};\n\nexport const useAuth = <\n  T extends Types.AuthRequest[\"type\"],\n  R extends Extract<Types.AuthRequest, { type: T }>,\n  P extends R[\"params\"],\n  C extends Omit<\n    UseMutationOptions<AuthResponses[T], unknown, P, unknown>,\n    \"mutationKey\" | \"mutationFn\"\n  >,\n>(\n  type: T,\n  config?: C\n) => {\n  const { toast } = useToast();\n  return useMutation({\n    mutationKey: [type],\n    mutationFn: (params: P) => komodo_client().auth<T, R>(type, params),\n    onError: (e: { result: { error?: string; trace?: string[] } }, v, c) => {\n      console.log(\"Auth error:\", e);\n      const msg = e.result.error ?? \"Unknown error. See console.\";\n      const detail = e.result?.trace\n        ?.map((msg) => msg[0].toUpperCase() + msg.slice(1))\n        .join(\" | \");\n      let msg_log = msg ? msg[0].toUpperCase() + msg.slice(1) + \" | \" : \"\";\n      if (detail) {\n        msg_log += detail + \" | \";\n      }\n      toast({\n        title: `Auth request ${type} failed`,\n        description: `${msg_log}See console for details`,\n        variant: \"destructive\",\n      });\n      config?.onError && config.onError(e, v, c);\n    },\n    ...config,\n  });\n};\n\n// ============== UTILITY ==============\n\nexport const useResourceParamType = () => {\n  const type = useParams().type;\n  if (!type) return undefined;\n  if (type === \"resource-syncs\") return \"ResourceSync\";\n  return (type[0].toUpperCase() + type.slice(1, -1)) as UsableResource;\n};\n\ntype ResourceMap = {\n  [Resource in UsableResource]: Types.ResourceListItem<unknown>[] | undefined;\n};\n\nexport const useAllResources = (): ResourceMap => {\n  return {\n    Server: useRead(\"ListServers\", {}).data,\n    Stack: useRead(\"ListStacks\", {}).data,\n    Deployment: useRead(\"ListDeployments\", {}).data,\n    Build: useRead(\"ListBuilds\", {}).data,\n    Repo: useRead(\"ListRepos\", {}).data,\n    Procedure: useRead(\"ListProcedures\", {}).data,\n    Action: useRead(\"ListActions\", {}).data,\n    Builder: useRead(\"ListBuilders\", {}).data,\n    Alerter: useRead(\"ListAlerters\", {}).data,\n    ResourceSync: useRead(\"ListResourceSyncs\", {}).data,\n  };\n};\n\n// Returns true if Komodo has no resources.\nexport const useNoResources = () => {\n  const resources = useAllResources();\n  for (const target of RESOURCE_TARGETS) {\n    if (resources[target] && resources[target].length) {\n      return false;\n    }\n  }\n  return true;\n};\n\n/** returns function that takes a resource target and checks if it exists */\nexport const useCheckResourceExists = () => {\n  const resources = useAllResources();\n  return (target: Types.ResourceTarget) => {\n    return (\n      resources[target.type as UsableResource]?.some(\n        (resource) => resource.id === target.id\n      ) || false\n    );\n  };\n};\n\nexport const useFilterResources = <Info>(\n  resources?: Types.ResourceListItem<Info>[],\n  search?: string\n) => {\n  const tags = useTagsFilter();\n  const searchSplit = search?.toLowerCase()?.split(\" \") || [];\n  return (\n    resources?.filter(\n      (resource) =>\n        tags.every((tag: string) => resource.tags.includes(tag)) &&\n        (searchSplit.length > 0\n          ? searchSplit.every((search) =>\n              resource.name.toLowerCase().includes(search)\n            )\n          : true)\n    ) ?? []\n  );\n};\n\nexport const usePushRecentlyViewed = ({ type, id }: Types.ResourceTarget) => {\n  const userInvalidate = useUserInvalidate();\n\n  const push = useManageUser(\"PushRecentlyViewed\", {\n    onSuccess: userInvalidate,\n  }).mutate;\n\n  const exists = useRead(`List${type as UsableResource}s`, {}).data?.find(\n    (r) => r.id === id\n  )\n    ? true\n    : false;\n\n  useEffect(() => {\n    exists && push({ resource: { type, id } });\n  }, [exists, push]);\n\n  return () => push({ resource: { type, id } });\n};\n\nexport const useSetTitle = (more?: string) => {\n  const info = useRead(\"GetCoreInfo\", {}).data;\n  const title = more ? `${more} | ${info?.title}` : info?.title;\n  useEffect(() => {\n    if (title) {\n      document.title = title;\n    }\n  }, [title]);\n};\n\nconst tagsAtom = atomWithStorage<string[]>(\"tags-v0\", []);\n\nexport const useTags = () => {\n  const [tags, setTags] = useAtom<string[]>(tagsAtom);\n\n  const add_tag = (tag_id: string) => setTags([...tags, tag_id]);\n  const remove_tag = (tag_id: string) =>\n    setTags(tags.filter((id) => id !== tag_id));\n  const toggle_tag = (tag_id: string) => {\n    if (tags.includes(tag_id)) {\n      remove_tag(tag_id);\n    } else {\n      add_tag(tag_id);\n    }\n  };\n  const clear_tags = () => setTags([]);\n\n  return {\n    tags,\n    add_tag,\n    remove_tag,\n    toggle_tag,\n    clear_tags,\n  };\n};\n\nexport const useTagsFilter = () => {\n  const [tags] = useAtom<string[]>(tagsAtom);\n  return tags;\n};\n\nexport type LocalStorageSetter<T> = (state: T) => T;\n\nexport const useLocalStorage = <T>(\n  key: string,\n  init: T\n): [T, (state: T | LocalStorageSetter<T>) => void] => {\n  const stored = localStorage.getItem(key);\n  const parsed = stored ? (JSON.parse(stored) as T) : undefined;\n  const [state, inner_set] = useState<T>(parsed ?? init);\n  const set = (state: T | LocalStorageSetter<T>) => {\n    inner_set((prev_state) => {\n      const new_val =\n        typeof state === \"function\"\n          ? (state as LocalStorageSetter<T>)(prev_state)\n          : state;\n      localStorage.setItem(key, JSON.stringify(new_val));\n      return new_val;\n    });\n  };\n  return [state, set];\n};\n\nexport const useKeyListener = (listenKey: string, onPress: () => void) => {\n  useEffect(() => {\n    const keydown = (e: KeyboardEvent) => {\n      // This will ignore Shift + listenKey if it is sent from input / textarea\n      const target = e.target as any;\n      if (target.matches(\"input\") || target.matches(\"textarea\")) return;\n\n      if (e.key === listenKey) {\n        e.preventDefault();\n        onPress();\n      }\n    };\n    document.addEventListener(\"keydown\", keydown);\n    return () => document.removeEventListener(\"keydown\", keydown);\n  });\n};\n\nexport const useShiftKeyListener = (listenKey: string, onPress: () => void) => {\n  useEffect(() => {\n    const keydown = (e: KeyboardEvent) => {\n      // This will ignore Shift + listenKey if it is sent from input / textarea\n      const target = e.target as any;\n      if (target.matches(\"input\") || target.matches(\"textarea\")) return;\n\n      if (e.shiftKey && e.key === listenKey) {\n        e.preventDefault();\n        onPress();\n      }\n    };\n    document.addEventListener(\"keydown\", keydown);\n    return () => document.removeEventListener(\"keydown\", keydown);\n  });\n};\n\n/** Listens for ctrl (or CMD on mac) + the listenKey */\nexport const useCtrlKeyListener = (listenKey: string, onPress: () => void) => {\n  useEffect(() => {\n    const keydown = (e: KeyboardEvent) => {\n      if ((e.ctrlKey || e.metaKey) && e.key === listenKey) {\n        e.preventDefault();\n        onPress();\n      }\n    };\n    document.addEventListener(\"keydown\", keydown);\n    return () => document.removeEventListener(\"keydown\", keydown);\n  });\n};\n\nexport interface PromptHotkeysConfig {\n  /** Function to call when Enter is pressed (confirm action) */\n  onConfirm?: () => void;\n  /** Function to call when Escape is pressed (cancel/close action) */\n  onCancel?: () => void;\n  /** Whether the hotkeys are enabled. Defaults to true */\n  enabled?: boolean;\n  /** Whether to ignore hotkeys when inside input/textarea elements. Defaults to true */\n  ignoreInputs?: boolean;\n  /** Whether the confirm action is disabled (e.g., form validation failed) */\n  confirmDisabled?: boolean;\n}\n\n/**\n * Hook that provides standard prompt/dialog hotkey behavior:\n * - Enter: Confirm/submit action\n * - Escape: Cancel/close action\n */\nexport const usePromptHotkeys = ({\n  enabled = true,\n  onConfirm,\n  onCancel,\n  ignoreInputs = true,\n  confirmDisabled = false,\n}: PromptHotkeysConfig) => {\n  useEffect(() => {\n    if (!enabled) return;\n\n    const findConfirmButton = (): HTMLButtonElement | null => {\n      const dialogContainers = document.querySelectorAll('[role=\"dialog\"], [data-state=\"open\"], .dialog-content');\n      for (const container of dialogContainers) {\n        const button = container.querySelector('[data-confirm-button]:not([disabled])') as HTMLButtonElement;\n        if (button) return button;\n      }\n\n      return document.querySelector('[data-confirm-button]:not([disabled])') as HTMLButtonElement;\n    };\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (ignoreInputs) {\n        const target = e.target as HTMLElement;\n        if (\n          target.tagName === \"INPUT\" ||\n          target.tagName === \"TEXTAREA\" ||\n          target.tagName === \"SELECT\" ||\n          target.isContentEditable\n        ) {\n          return;\n        }\n      }\n\n      switch (e.key) {\n        case \"Enter\":\n          if (onConfirm && !confirmDisabled) {\n            e.preventDefault();\n            const confirmButton = findConfirmButton();\n            if (confirmButton) {\n              confirmButton.click();\n            } else {\n              onConfirm();\n            }\n          }\n          break;\n        case \"Escape\":\n          if (onCancel) {\n            e.preventDefault();\n            onCancel();\n          }\n          break;\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n  }, [enabled, onConfirm, onCancel, ignoreInputs, confirmDisabled]);\n};\n\nexport type WebhookIntegration = \"Github\" | \"Gitlab\";\nexport type WebhookIntegrations = {\n  [key: string]: WebhookIntegration;\n};\n\nconst WEBHOOK_INTEGRATIONS_ATOM = atomWithStorage<WebhookIntegrations>(\n  \"webhook-integrations-v2\",\n  {}\n);\n\nexport const useWebhookIntegrations = () => {\n  const [integrations, setIntegrations] = useAtom<WebhookIntegrations>(\n    WEBHOOK_INTEGRATIONS_ATOM\n  );\n  return {\n    integrations,\n    setIntegration: (provider: string, integration: WebhookIntegration) =>\n      setIntegrations({\n        ...integrations,\n        [provider]: integration,\n      }),\n  };\n};\n\nexport const getWebhookIntegration = (\n  integrations: WebhookIntegrations,\n  git_provider: string\n) => {\n  return integrations[git_provider]\n    ? integrations[git_provider]\n    : git_provider.includes(\"gitlab\")\n      ? \"Gitlab\"\n      : \"Github\";\n};\n\nexport type WebhookIdOrName = \"Id\" | \"Name\";\n\nconst WEBHOOK_ID_OR_NAME_ATOM = atomWithStorage<WebhookIdOrName>(\n  \"webhook-id-or-name-v1\",\n  \"Id\"\n);\n\nexport const useWebhookIdOrName = () => {\n  return useAtom<WebhookIdOrName>(WEBHOOK_ID_OR_NAME_ATOM);\n};\n\nexport type Dimensions = { width: number; height: number };\nexport const useWindowDimensions = () => {\n  const [dimensions, setDimensions] = useState<Dimensions>({\n    width: 0,\n    height: 0,\n  });\n  useEffect(() => {\n    const callback = () => {\n      setDimensions({\n        width: window.screen.availWidth,\n        height: window.screen.availHeight,\n      });\n    };\n    callback();\n    window.addEventListener(\"resize\", callback);\n    return () => {\n      window.removeEventListener(\"resize\", callback);\n    };\n  }, []);\n  return dimensions;\n};\n\nconst selected_resources = atomFamily((_: UsableResource) =>\n  atom<string[]>([])\n);\nexport const useSelectedResources = (type: UsableResource) =>\n  useAtom(selected_resources(type));\n\nconst filter_by_update_available = atomWithStorage<boolean>(\n  \"update-available-filter-v1\",\n  false\n);\nexport const useFilterByUpdateAvailable: () => [boolean, () => void] = () => {\n  const [filter, set] = useAtom<boolean>(filter_by_update_available);\n  return [filter, () => set(!filter)];\n};\n\nexport const usePermissions = ({ type, id }: Types.ResourceTarget) => {\n  const user = useUser().data;\n  const perms = useRead(\"GetPermission\", { target: { type, id } }).data as\n    | Types.PermissionLevelAndSpecifics\n    | Types.PermissionLevel\n    | undefined;\n  const info = useRead(\"GetCoreInfo\", {}).data;\n  const ui_write_disabled = info?.ui_write_disabled ?? false;\n  const disable_non_admin_create = info?.disable_non_admin_create ?? false;\n\n  const level =\n    (perms && typeof perms === \"string\" ? perms : perms?.level) ??\n    Types.PermissionLevel.None;\n  const specific =\n    (perms && typeof perms === \"string\" ? [] : perms?.specific) ?? [];\n\n  const canWrite = !ui_write_disabled && level === Types.PermissionLevel.Write;\n  const canExecute = has_minimum_permissions(\n    { level, specific },\n    Types.PermissionLevel.Execute\n  );\n\n  const [\n    specificLogs,\n    specificInspect,\n    specificTerminal,\n    specificAttach,\n    specificProcesses,\n  ] = [\n    specific.includes(Types.SpecificPermission.Logs),\n    specific.includes(Types.SpecificPermission.Inspect),\n    specific.includes(Types.SpecificPermission.Terminal),\n    specific.includes(Types.SpecificPermission.Attach),\n    specific.includes(Types.SpecificPermission.Processes),\n  ];\n\n  const canCreate =\n    type === \"Server\"\n      ? user?.admin ||\n        (!disable_non_admin_create && user?.create_server_permissions)\n      : type === \"Build\"\n        ? user?.admin ||\n          (!disable_non_admin_create && user?.create_build_permissions)\n        : type === \"Alerter\" ||\n            type === \"Builder\" ||\n            type === \"Procedure\" ||\n            type === \"Action\"\n          ? user?.admin\n          : user?.admin || !disable_non_admin_create;\n\n  return {\n    canWrite,\n    canExecute,\n    canCreate,\n    specific,\n    specificLogs,\n    specificInspect,\n    specificTerminal,\n    specificAttach,\n    specificProcesses,\n  };\n};\n\nconst templatesQueryBehaviorAtom =\n  atomWithStorage<Types.TemplatesQueryBehavior>(\n    \"templates-query-behavior-v0\",\n    Types.TemplatesQueryBehavior.Exclude\n  );\n\nexport const useTemplatesQueryBehavior = () =>\n  useAtom<Types.TemplatesQueryBehavior>(templatesQueryBehaviorAtom);\n\nexport type SettingsView =\n  | \"Variables\"\n  | \"Tags\"\n  | \"Providers\"\n  | \"Users\"\n  | \"Profile\";\n\nconst viewAtom = atomWithStorage<SettingsView>(\"settings-view-v2\", \"Variables\");\n\nexport const useSettingsView = () => useAtom<SettingsView>(viewAtom);\n\n/**\n * Map of unique host ports to array of formatted full port map spec\n * Formatted ex: 0.0.0.0:3000:3000/tcp\n */\nexport type PortsMap = { [host_port: string]: Array<Types.Port> };\n\nexport const useContainerPortsMap = (ports: Types.Port[]) => {\n  return useMemo(() => {\n    const map: PortsMap = {};\n    for (const port of ports) {\n      if (!port.PublicPort || !port.PrivatePort) continue;\n      if (map[port.PublicPort]) {\n        map[port.PublicPort].push(port);\n      } else {\n        map[port.PublicPort] = [port];\n      }\n    }\n    for (const key in map) {\n      map[key].sort();\n    }\n    return map;\n  }, [ports]);\n};\n\n/**\n * A custom React hook that debounces a value, delaying its update until after\n * a specified period of inactivity. This is useful for performance optimization\n * in scenarios like search inputs, form validation, or API calls.\n */\nexport function useDebounce<T>(value: T, delay: number): T {\n  const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n  useEffect(() => {\n    const handler = setTimeout(() => {\n      setDebouncedValue(value);\n    }, delay);\n\n    return () => {\n      clearTimeout(handler);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n}"
  },
  {
    "path": "frontend/src/lib/socket.tsx",
    "content": "import { useInvalidate, komodo_client, useRead, useUser } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Button } from \"@ui/button\";\nimport { toast } from \"@ui/use-toast\";\nimport { atom, useAtom } from \"jotai\";\nimport { Circle } from \"lucide-react\";\nimport { ReactNode, useCallback, useEffect, useRef } from \"react\";\nimport { cn } from \"@lib/utils\";\nimport { ResourceComponents } from \"@components/resources\";\nimport { UsableResource } from \"@types\";\nimport { ResourceNameSimple } from \"@components/resources/common\";\n\nconst ws_atom = atom<{\n  ws: WebSocket | undefined;\n  connected: boolean;\n  count: number;\n}>({\n  ws: undefined,\n  connected: false,\n  count: 0,\n});\n\nexport const useWebsocketConnected = () => useAtom(ws_atom)[0].connected;\n\nconst useWebsocketReconnect = () => {\n  const [ws, set] = useAtom(ws_atom);\n\n  return () => {\n    if (ws.ws?.readyState === WebSocket.OPEN) {\n      ws.ws?.close();\n    }\n    set((ws) => ({\n      ws: undefined,\n      connected: false,\n      count: ws.count + 1,\n    }));\n  };\n};\n\nconst onMessageHandlers: {\n  [key: string]: (update: Types.UpdateListItem) => void;\n} = {};\n\nexport const useWebsocketMessages = (\n  key: string,\n  handler: (update: Types.UpdateListItem) => void\n) => {\n  onMessageHandlers[key] = handler;\n  useEffect(() => {\n    // Clean up on unmount\n    return () => {\n      delete onMessageHandlers[key];\n    };\n  }, []);\n};\n\nexport const WebsocketProvider = ({ children }: { children: ReactNode }) => {\n  const user = useUser().data;\n  const invalidate = useInvalidate();\n  const [ws, setWs] = useAtom(ws_atom);\n  const countRef = useRef<number>(ws.count);\n  const reconnect = useWebsocketReconnect();\n  const disable_reconnect = useRead(\"GetCoreInfo\", {}).data\n    ?.disable_websocket_reconnect;\n\n  useEffect(() => {\n    countRef.current = ws.count;\n  }, [ws.count]);\n\n  const on_update_fn = useCallback(\n    (update: Types.UpdateListItem) => on_update(update, invalidate),\n    [invalidate]\n  );\n\n  useEffect(() => {\n    if (user && disable_reconnect !== undefined && ws.ws === undefined) {\n      // make a copy of the count to not change.\n      const count = ws.count;\n      let timeout = -1;\n      const socket = komodo_client().get_update_websocket({\n        on_login: () => {\n          console.info(count, \"| Logged into Update websocket\");\n          setWs((ws) => ({ ...ws, connected: true }));\n        },\n        on_update: on_update_fn,\n        on_close: () => {\n          console.info(count, \"| Update websocket connection closed\");\n          setWs((ws) => ({ ...ws, connected: false }));\n          if (!disable_reconnect) {\n            timeout = setTimeout(() => {\n              if (countRef.current === count) {\n                console.info(count, \"| Automatically triggering reconnect\");\n                reconnect();\n              }\n            }, 5_000);\n          }\n        },\n      });\n      setWs((ws) => ({ ...ws, ws: socket }));\n      return () => clearTimeout(timeout);\n    }\n  }, [user, disable_reconnect, ws.ws, ws.count]);\n\n  return <>{children}</>;\n};\n\nexport const WsStatusIndicator = () => {\n  const [ws] = useAtom(ws_atom);\n  const reconnect = useWebsocketReconnect();\n  const onclick = () => {\n    reconnect();\n    toast({\n      title: ws.connected\n        ? \"Triggered websocket reconnect\"\n        : \"Triggered websocket connect\",\n    });\n  };\n\n  return (\n    <Button\n      variant=\"ghost\"\n      onClick={onclick}\n      size=\"icon\"\n      className=\"hidden lg:inline-flex\"\n    >\n      <Circle\n        className={cn(\n          \"w-4 h-4 stroke-none transition-colors\",\n          ws.connected ? \"fill-green-500\" : \"fill-red-500\"\n        )}\n      />\n    </Button>\n  );\n};\n\nconst on_update = (\n  update: Types.UpdateListItem,\n  invalidate: ReturnType<typeof useInvalidate>\n) => {\n  const Components = ResourceComponents[update.target.type as UsableResource];\n  const title = Components ? (\n    <div className=\"flex items-center gap-2\">\n      <div>Update</div> -<div>{update.operation}</div> -\n      <div>\n        <ResourceNameSimple\n          type={update.target.type as UsableResource}\n          id={update.target.id}\n        />\n      </div>\n      {!update.success && <div>FAILED</div>}\n    </div>\n  ) : (\n    `${update.operation}${update.success ? \"\" : \" - FAILED\"}`\n  );\n\n  toast({ title: title as any });\n\n  // Invalidate these every time\n  invalidate([\"ListUpdates\"]);\n  invalidate([\"GetUpdate\", { id: update.id }]);\n  if (update.target.type === \"Deployment\") {\n    invalidate([\"GetDeploymentActionState\", { deployment: update.target.id }]);\n  } else if (update.target.type === \"Stack\") {\n    invalidate([\"GetStackActionState\", { stack: update.target.id }]);\n  } else if (update.target.type === \"Server\") {\n    invalidate([\"GetServerActionState\", { server: update.target.id }]);\n  } else if (update.target.type === \"Build\") {\n    invalidate([\"GetBuildActionState\", { build: update.target.id }]);\n  } else if (update.target.type === \"Repo\") {\n    invalidate([\"GetRepoActionState\", { repo: update.target.id }]);\n  } else if (update.target.type === \"Procedure\") {\n    invalidate([\"GetProcedureActionState\", { procedure: update.target.id }]);\n  } else if (update.target.type === \"Action\") {\n    invalidate([\"GetActionActionState\", { action: update.target.id }]);\n  } else if (update.target.type === \"ResourceSync\") {\n    invalidate([\"GetResourceSyncActionState\", { sync: update.target.id }]);\n  }\n\n  // Invalidate lists for execution updates - update status\n  if (update.operation === Types.Operation.RunBuild) {\n    invalidate([\"ListBuilds\"]);\n  } else if (\n    [\n      Types.Operation.CloneRepo,\n      Types.Operation.PullRepo,\n      Types.Operation.BuildRepo,\n    ].includes(update.operation)\n  ) {\n    invalidate([\"ListRepos\"]);\n  } else if (update.operation === Types.Operation.RunProcedure) {\n    invalidate([\"ListProcedures\"]);\n  } else if (update.operation === Types.Operation.RunAction) {\n    invalidate([\"ListActions\"]);\n  }\n\n  // Do invalidations of these only if update is completed\n  if (update.status === Types.UpdateStatus.Complete) {\n    invalidate([\"ListAlerts\"]);\n\n    // Invalidate docker infos\n    if ([\"Server\", \"Deployment\", \"Stack\"].includes(update.target.type)) {\n      invalidate(\n        [\"ListDockerContainers\"],\n        [\"InspectDockerContainer\"],\n        [\"ListDockerNetworks\"],\n        [\"InspectDockerNetwork\"],\n        [\"ListDockerImages\"],\n        [\"InspectDockerImage\"],\n        [\"ListDockerVolumes\"],\n        [\"InspectDockerVolume\"],\n        [\"GetResourceMatchingContainer\"]\n      );\n    }\n\n    if (update.target.type === \"Deployment\") {\n      invalidate(\n        [\"ListDeployments\"],\n        [\"GetDeploymentsSummary\"],\n        [\"ListDockerContainers\"],\n        [\"ListDockerNetworks\"],\n        [\"ListDockerImages\"],\n        [\"GetDeployment\"],\n        [\"GetDeploymentLog\", { deployment: update.target.id }],\n        [\"SearchDeploymentLog\", { deployment: update.target.id }],\n        [\"GetDeploymentContainer\"],\n        [\"GetResourceMatchingContainer\"]\n      );\n    }\n\n    if (update.target.type === \"Stack\") {\n      invalidate(\n        [\"ListStacks\"],\n        [\"ListFullStacks\"],\n        [\"GetStacksSummary\"],\n        [\"ListCommonStackExtraArgs\"],\n        [\"ListComposeProjects\"],\n        [\"ListDockerContainers\"],\n        [\"ListDockerNetworks\"],\n        [\"ListDockerImages\"],\n        [\"GetStackLog\", { stack: update.target.id }],\n        [\"SearchStackLog\", { stack: update.target.id }],\n        [\"GetStack\"],\n        [\"ListStackServices\"],\n        [\"GetResourceMatchingContainer\"]\n      );\n    }\n\n    if (update.target.type === \"Server\") {\n      invalidate(\n        [\"ListServers\"],\n        [\"ListFullServers\"],\n        [\"GetServersSummary\"],\n        [\"GetServer\"],\n        [\"GetServerState\"],\n        [\"GetHistoricalServerStats\"]\n      );\n    }\n\n    if (update.target.type === \"Build\") {\n      invalidate(\n        [\"ListBuilds\"],\n        [\"ListFullBuilds\"],\n        [\"GetBuildsSummary\"],\n        [\"GetBuildMonthlyStats\"],\n        [\"GetBuild\"],\n        [\"ListBuildVersions\"]\n      );\n    }\n\n    if (update.target.type === \"Repo\") {\n      invalidate(\n        [\"ListRepos\"],\n        [\"ListFullRepos\"],\n        [\"GetReposSummary\"],\n        [\"GetRepo\"]\n      );\n    }\n\n    if (update.target.type === \"Procedure\") {\n      invalidate(\n        [\"ListSchedules\"],\n        [\"ListProcedures\"],\n        [\"ListFullProcedures\"],\n        [\"GetProceduresSummary\"],\n        [\"GetProcedure\"]\n      );\n    }\n\n    if (update.target.type === \"Action\") {\n      invalidate(\n        [\"ListSchedules\"],\n        [\"ListActions\"],\n        [\"ListFullActions\"],\n        [\"GetActionsSummary\"],\n        [\"GetAction\"]\n      );\n    }\n\n    if (update.target.type === \"Builder\") {\n      invalidate(\n        [\"ListBuilders\"],\n        [\"ListFullBuilders\"],\n        [\"GetBuildersSummary\"],\n        [\"GetBuilder\"]\n      );\n    }\n\n    if (update.target.type === \"Alerter\") {\n      invalidate(\n        [\"ListAlerters\"],\n        [\"ListFullAlerters\"],\n        [\"GetAlertersSummary\"],\n        [\"GetAlerter\"]\n      );\n    }\n\n    if (update.target.type === \"ResourceSync\") {\n      invalidate(\n        [\"ListResourceSyncs\"],\n        [\"ListFullResourceSyncs\"],\n        [\"GetResourceSyncsSummary\"],\n        [\"GetResourceSync\"]\n      );\n    }\n\n    if (\n      update.target.type === \"System\" &&\n      update.operation.includes(\"Variable\")\n    ) {\n      invalidate([\"ListVariables\"], [\"GetVariable\"]);\n    }\n  }\n\n  // Run any attached handlers\n  Object.values(onMessageHandlers).forEach((handler) => handler(update));\n};\n"
  },
  {
    "path": "frontend/src/lib/utils.ts",
    "content": "import { ResourceComponents } from \"@components/resources\";\nimport { Types } from \"komodo_client\";\nimport { UsableResource } from \"@types\";\nimport Convert from \"ansi-to-html\";\nimport { type ClassValue, clsx } from \"clsx\";\nimport sanitizeHtml from \"sanitize-html\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport const object_keys = <T extends object>(o: T): (keyof T)[] =>\n  Object.keys(o) as (keyof T)[];\n\nexport const RESOURCE_TARGETS: UsableResource[] = [\n  \"Server\",\n  \"Stack\",\n  \"Deployment\",\n  \"Build\",\n  \"Repo\",\n  \"Procedure\",\n  \"Action\",\n  \"Builder\",\n  \"Alerter\",\n  \"ResourceSync\",\n];\n\nexport const SETTINGS_RESOURCES: UsableResource[] = [\"Builder\", \"Alerter\"];\n\nexport const SIDEBAR_RESOURCES: UsableResource[] = RESOURCE_TARGETS.filter(\n  (target) => !SETTINGS_RESOURCES.includes(target)\n);\n\nexport function env_to_text(envVars: Types.EnvironmentVar[] | undefined) {\n  return envVars?.reduce(\n    (prev, { variable, value }) =>\n      prev + (prev ? \"\\n\" : \"\") + `${variable}: ${value}`,\n    \"\"\n  );\n}\n\nexport function text_to_env(env: string): Types.EnvironmentVar[] {\n  return env\n    .split(\"\\n\")\n    .filter((line) => keep_line(line))\n    .map((entry) => {\n      const [first, ...rest] = entry.replaceAll('\"', \"\").split(\"=\");\n      return [first, rest.join(\"=\")];\n    })\n    .map(([variable, value]) => ({ variable, value }));\n}\n\nfunction keep_line(line: string) {\n  if (line.length === 0) return false;\n  let firstIndex = -1;\n  for (let i = 0; i < line.length; i++) {\n    if (line[i] !== \" \") {\n      firstIndex = i;\n      break;\n    }\n  }\n  if (firstIndex === -1) return false;\n  if (line[firstIndex] === \"#\") return false;\n  return true;\n}\n\nexport function parse_key_value(\n  input: string\n): Array<{ key: string; value: string }> {\n  const trimmed = input.trim();\n  if (trimmed.length === 0) return [];\n  return trimmed\n    .split(\"\\n\")\n    .map((line) => line.trim())\n    .filter(\n      (line) =>\n        line.length > 0 && !line.startsWith(\"#\") && !line.startsWith(\"//\")\n    )\n    .map((line) => {\n      const no_comment = line.split(\" #\", 1)[0].trim();\n      const no_dash = no_comment.startsWith(\"-\")\n        ? no_comment.slice(1).trim()\n        : no_comment;\n      const no_leading_quote = no_dash.startsWith('\"')\n        ? no_dash.slice(1)\n        : no_dash;\n      const no_trailing_quote = no_leading_quote.endsWith('\"')\n        ? no_leading_quote.slice(0, -1)\n        : no_leading_quote;\n      const res = no_trailing_quote.split(/[=: ]/, 1);\n      const [key, value] = [res[0]?.trim() ?? \"\", res[1]?.trim() ?? \"\"];\n      const value_no_leading_quote = value.startsWith('\"')\n        ? value.slice(1)\n        : value;\n      const value_no_trailing_quote = value_no_leading_quote.endsWith('\"')\n        ? value_no_leading_quote.slice(0, -1)\n        : value_no_leading_quote;\n      return { key, value: value_no_trailing_quote.trim() };\n    });\n}\n\nexport function version_is_none(version?: Types.Version) {\n  if (!version) return true;\n  return version.major === 0 && version.minor === 0 && version.patch === 0;\n}\n\nexport function resource_name(type: UsableResource, id: string) {\n  const Components = ResourceComponents[type];\n  return Components.list_item(id)?.name;\n}\n\nexport const level_to_number = (level: Types.PermissionLevel | undefined) => {\n  switch (level) {\n    case undefined:\n      return 0;\n    case Types.PermissionLevel.None:\n      return 0;\n    case Types.PermissionLevel.Read:\n      return 1;\n    case Types.PermissionLevel.Execute:\n      return 2;\n    case Types.PermissionLevel.Write:\n      return 3;\n  }\n};\n\nexport const has_minimum_permissions = (\n  permission: Types.PermissionLevelAndSpecifics | undefined,\n  greater_than: Types.PermissionLevel,\n  specific?: Types.SpecificPermission[]\n) => {\n  if (!permission) return false;\n  if (level_to_number(permission.level) < level_to_number(greater_than))\n    return false;\n  if (!specific) return true;\n  for (const s of specific) {\n    if (!permission.specific.includes(s)) {\n      return false;\n    }\n  }\n  return true;\n};\n\nconst tzOffsetMs = new Date().getTimezoneOffset() * 60 * 1000;\n\nexport const convertTsMsToLocalUnixTsInMs = (ts: number) => ts - tzOffsetMs;\n\nexport const usableResourcePath = (resource: UsableResource) => {\n  if (resource === \"ResourceSync\") return \"resource-syncs\";\n  return `${resource.toLowerCase()}s`;\n};\n\nexport const usableResourceExecuteKey = (resource: UsableResource) => {\n  if (resource === \"ResourceSync\") return \"sync\";\n  return `${resource.toLowerCase()}`;\n};\n\nexport const sanitizeOnlySpan = (log: string) => {\n  return sanitizeHtml(log, {\n    allowedTags: [\"span\"],\n    allowedAttributes: {\n      span: [\"class\"],\n    },\n  });\n};\n\n/**\n * Converts the ansi colors in an Update log to html.\n * sanitizes incoming log first for any eg. script tags.\n * @param log incoming log string\n */\nexport const updateLogToHtml = (log: string) => {\n  if (!log) return \"No log.\";\n  return convert.toHtml(sanitizeOnlySpan(log));\n};\n\nconst convert = new Convert();\n/**\n * Converts the ansi colors in log to html.\n * sanitizes incoming log first for any eg. script tags.\n * @param log incoming log string\n */\nexport const logToHtml = (log: string) => {\n  if (!log) return \"No log.\";\n  const sanitized = sanitizeHtml(log, {\n    allowedTags: sanitizeHtml.defaults.allowedTags.filter(\n      (tag) => tag !== \"script\"\n    ),\n    allowedAttributes: sanitizeHtml.defaults.allowedAttributes,\n  });\n  return convert.toHtml(sanitized);\n};\n\nexport const getUpdateQuery = (\n  target: Types.ResourceTarget,\n  deployments: Types.DeploymentListItem[] | undefined\n) => {\n  const build_id =\n    target.type === \"Deployment\"\n      ? deployments?.find((d) => d.id === target.id)?.info.build_id\n      : undefined;\n  if (build_id) {\n    return {\n      $or: [\n        {\n          \"target.type\": target.type,\n          \"target.id\": target.id,\n        },\n        {\n          \"target.type\": \"Build\",\n          \"target.id\": build_id,\n          operation: {\n            $in: [Types.Operation.RunBuild, Types.Operation.CancelBuild],\n          },\n        },\n      ],\n    };\n  } else {\n    return {\n      \"target.type\": target.type,\n      \"target.id\": target.id,\n    };\n  }\n};\n\nexport const filterBySplit = <T>(\n  items: T[] | undefined,\n  search: string,\n  extract: (item: T) => string\n) => {\n  const split = search.toLowerCase().split(\" \");\n  return (\n    (split.length\n      ? items?.filter((item) => {\n          const target = extract(item).toLowerCase();\n          return split.every((term) => target.includes(term));\n        })\n      : items) ?? []\n  );\n};\n\nexport const sync_no_changes = (sync: Types.ResourceSync) => {\n  return (\n    (sync.info?.pending_deploy?.to_deploy ?? 0) === 0 &&\n    (sync.info?.resource_updates?.length ?? 0) === 0 &&\n    (sync.info?.variable_updates?.length ?? 0) === 0 &&\n    (sync.info?.user_group_updates?.length ?? 0) === 0\n  );\n};\n\nexport const extract_registry_domain = (image_name: string) => {\n  if (!image_name) return \"docker.io\";\n  const maybe_domain = image_name.split(\"/\")[0];\n  if (maybe_domain.includes(\".\")) {\n    return maybe_domain;\n  } else {\n    return \"docker.io\";\n  }\n};\n\n/** Checks file contents empty, not including whitespace / comments */\nexport const file_contents_empty = (contents?: string) => {\n  if (!contents) return true;\n  return (\n    contents\n      .split(\"\\n\")\n      .map((line) => line.trim())\n      .filter((line) => line.length !== 0 && !line.startsWith(\"#\")).length === 0\n  );\n};\n"
  },
  {
    "path": "frontend/src/main.tsx",
    "content": "import \"globals.css\";\nimport ReactDOM from \"react-dom/client\";\nimport { ThemeProvider } from \"@ui/theme\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { Router } from \"@router\";\nimport { WebsocketProvider } from \"@lib/socket\";\nimport { Toaster } from \"@ui/toaster\";\nimport { atomWithStorage } from \"@lib/hooks\";\n// Run monaco setup\nimport \"./monaco\";\nimport { init_monaco } from \"./monaco/init\";\n\nexport const KOMODO_BASE_URL =\n  import.meta.env.VITE_KOMODO_HOST ?? location.origin;\nexport const UPDATE_WS_URL =\n  KOMODO_BASE_URL.replace(\"http\", \"ws\") + \"/ws/update\";\nconst query_client = new QueryClient({\n  defaultOptions: { queries: { retry: false } },\n});\n\nexport type HomeView = \"Dashboard\" | \"Tree\" | \"Resources\";\n\nexport const homeViewAtom = atomWithStorage<HomeView>(\n  \"home-view-v1\",\n  \"Dashboard\"\n);\n\n// Don't need to await this to render.\ninit_monaco();\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  // <React.StrictMode>\n  <QueryClientProvider client={query_client}>\n    <WebsocketProvider>\n      <ThemeProvider>\n        <Router />\n        <Toaster />\n      </ThemeProvider>\n    </WebsocketProvider>\n  </QueryClientProvider>\n  // </React.StrictMode>\n);\n"
  },
  {
    "path": "frontend/src/monaco/fancy_toml.ts",
    "content": "import * as monaco from \"monaco-editor\";\n\n/// V2: Toml + Yaml + Env Vars\nconst fancy_toml_conf: monaco.languages.LanguageConfiguration = {\n  comments: {\n    lineComment: \"#\",\n  },\n  brackets: [\n    [\"{\", \"}\"],\n    [\"[\", \"]\"],\n    [\"(\", \")\"],\n  ],\n  autoClosingPairs: [\n    { open: \"{\", close: \"}\" },\n    { open: \"[\", close: \"]\" },\n    { open: \"(\", close: \")\" },\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n    { open: '\"\"\"', close: '\"\"\"' },\n  ],\n  surroundingPairs: [\n    { open: \"{\", close: \"}\" },\n    { open: \"[\", close: \"]\" },\n    { open: \"(\", close: \")\" },\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n    { open: '\"\"\"', close: '\"\"\"' },\n  ],\n};\n\nconst fancy_toml_language = <monaco.languages.IMonarchLanguage>{\n  defaultToken: \"\",\n  tokenPostfix: \".toml\",\n\n  escapes: /\\\\(?:[btnfr\\\"\\'\\\\\\/]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,\n\n  tokenizer: {\n    root: [\n      // Comments\n      [/\\s*((#).*)$/, \"comment\"],\n\n      // Table Definitions\n      [\n        /^\\s*(\\[\\[)([^[\\]]+)(\\]\\])/,\n        [\n          \"punctuation.definition.array.table\",\n          \"entity.other.attribute-name.table.array\",\n          \"punctuation.definition.array.table\",\n        ],\n      ],\n      [\n        /^\\s*(\\[)([^[\\]]+)(\\])/,\n        [\n          \"punctuation.definition.table\",\n          \"entity.other.attribute-name.table\",\n          \"punctuation.definition.table\",\n        ],\n      ],\n\n      // Inline tables\n      [\n        /\\{/,\n        {\n          token: \"punctuation.definition.table.inline\",\n          next: \"@inlineTable\",\n        },\n      ],\n\n      // Entry (Key = Value)\n      [\n        /\\s*((?:(?:(?:[A-Za-z0-9_+-]+)|(?:\\\"[^\\\"]+\\\")|(?:'[^']+'))\\s*\\.?\\s*)+)\\s*(=)/,\n        [\"\", \"delimiter\"],\n      ],\n\n      // Values (booleans, numbers, dates, strings, arrays)\n      { include: \"@values\" },\n    ],\n\n    // Inline Table\n    inlineTable: [\n      [/\\}/, { token: \"punctuation.definition.table.inline\", next: \"@pop\" }],\n      { include: \"@comments\" },\n      [/,/, \"punctuation.separator.table.inline\"],\n      { include: \"@values\" },\n    ],\n\n    // Values (Strings, Numbers, Booleans, Dates, Arrays)\n    values: [\n      // Triple quoted string (basic)\n      [/\"\"\"/, { token: \"string\", next: \"@tripleStringWithYamlEnv\" }],\n\n      // Single quoted string\n      [/\"/, { token: \"string.quoted.single.basic.line\", next: \"@basicString\" }],\n\n      // Triple quoted literal string\n      [\n        /'''/,\n        {\n          token: \"string.quoted.triple.literal.block\",\n          next: \"@literalTripleStringWithYamlEnv\",\n        },\n      ],\n\n      // Single quoted literal string\n      [\n        /'/,\n        {\n          token: \"string.quoted.single.literal.line\",\n          next: \"@literalStringSingle\",\n        },\n      ],\n\n      // Dates and Times\n      [\n        /\\d{4}-\\d{2}-\\d{2}[Tt ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})/,\n        \"constant.other.time.datetime.offset\",\n      ],\n      [\n        /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?/,\n        \"constant.other.time.datetime.local\",\n      ],\n      [/\\d{4}-\\d{2}-\\d{2}/, \"constant.other.time.date\"],\n      [/\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?/, \"constant.other.time.time\"],\n\n      // Booleans\n      [/\\b(true|false)\\b/, \"constant.language.boolean\"],\n\n      // Numbers\n      [/[+-]?(0x[0-9A-Fa-f_]+|0o[0-7_]+|0b[01_]+)/, \"number.hex\"],\n      [\n        /(?<!\\w)([+-]?(0|([1-9](([0-9]|_[0-9])+)?))(?:(?:\\.(0|([1-9](([0-9]|_[0-9])+)?)))?[eE][+-]?[1-9]_?[0-9]*|(?:\\.[0-9_]*)))(?!\\w)/,\n        \"number.float\",\n      ],\n      [/(?<!\\w)((?:[+-]?(0|([1-9](([0-9]|_[0-9])+)?))))(?!\\w)/, \"number\"],\n\n      // Arrays\n      [/\\[/, { token: \"punctuation.definition.array\", next: \"@array\" }],\n    ],\n\n    // Basic quoted string\n    basicString: [\n      [/[^\\\\\"]+/, \"string\"],\n      [/@escapes/, \"constant.character.escape\"],\n      [/\\\\./, \"invalid\"],\n      [/\"/, { token: \"string.quoted.single.basic.line\", next: \"@pop\" }],\n    ],\n\n    // Literal triple quoted string\n    literalStringTriple: [\n      [/[^']+/, \"string\"],\n      [/'/, { token: \"string.quoted.triple.literal.block\", next: \"@pop\" }],\n    ],\n\n    // Literal single quoted string\n    literalStringSingle: [\n      [/[^']+/, \"string\"],\n      [/'/, { token: \"string.quoted.single.literal.line\", next: \"@pop\" }],\n    ],\n\n    // Arrays\n    array: [\n      [/\\]/, { token: \"punctuation.definition.array\", next: \"@pop\" }],\n      [/,/, \"punctuation.separator.array\"],\n      { include: \"@values\" },\n    ],\n\n    // Handle whitespace and comments\n    whitespace: [[/\\s+/, \"\"]],\n    comments: [[/\\s*((#).*)$/, \"comment.line.number-sign\"]],\n\n    // CUSTOM STUFF FOR YAML / ENV IN TRIPLE STRING\n\n    tripleStringWithYamlEnv: [\n      [/\"\"\"/, { token: \"string\", next: \"@pop\" }],\n      { include: \"@yamlTokenizer\" }, // YAML inside triple quotes\n      { include: \"@envVariableTokenizer\" }, // Environment Variable parsing inside triple quotes\n    ],\n\n    literalTripleStringWithYamlEnv: [\n      [/'''/, { token: \"string\", next: \"@pop\" }],\n      { include: \"@yamlTokenizer\" }, // YAML inside triple quotes\n      { include: \"@envVariableTokenizer\" }, // Environment Variable parsing inside triple quotes\n    ],\n\n    // YAML Tokenizer for inside triple quotes\n    yamlTokenizer: [\n      { include: \"@yaml_whitespace\" },\n      { include: \"@yaml_comments\" },\n      { include: \"@yaml_keys\" },\n      { include: \"@yaml_numbers\" },\n      { include: \"@yaml_booleans\" },\n      { include: \"@yaml_strings\" },\n      { include: \"@yaml_constants\" },\n    ],\n\n    // Environment Variable Tokenizer\n    envVariableTokenizer: [\n      [\n        /(\\s*-*\\s*)([A-Za-z0-9_]+)(\\s*)(=|:)(\\s*)/,\n        [\"\", \"key\", \"\", \"operator.assignment\", \"\"],\n      ],\n      { include: \"@yamlTokenizer\" }, // Use YAML tokenizer for EnvVar values\n    ],\n\n    yaml_whitespace: [[/[ \\t\\r\\n]+/, \"\"]],\n    yaml_comments: [[/#.*$/, \"comment\"]],\n    yaml_keys: [[/([^\\s\\[\\]{},\"']+)(\\s*)(:)/, [\"key\", \"\", \"delimiter\"]]],\n    yaml_numbers: [\n      [/\\b\\d+\\.\\d*\\b/, \"number.float\"],\n      [/\\b0x[0-9a-fA-F]+\\b/, \"number.hex\"],\n      [/\\b\\d+\\b/, \"number\"],\n    ],\n    yaml_booleans: [\n      [/\\b(true|false|yes|no|on|off)\\b/, \"constant.language.boolean\"],\n    ],\n    yaml_strings: [\n      [/\"([^\"\\\\]|\\\\.)*$/, \"string.invalid\"], // Non-terminated string\n      [/'([^'\\\\]|\\\\.)*$/, \"string.invalid\"], // Non-terminated string\n      [/\"/, \"string\", \"@yaml_string_double\"],\n      [/'/, \"string\", \"@yaml_string_single\"],\n    ],\n    yaml_string_double: [\n      [/[^\\\\\"]+/, \"string\"],\n      [/@escapes/, \"string.escape\"],\n      [/\\\\./, \"string.escape.invalid\"],\n      [/\"/, { token: \"string\", next: \"@pop\" }],\n    ],\n    yaml_string_single: [\n      [/[^\\\\']+/, \"string\"],\n      [/@escapes/, \"string.escape\"],\n      [/\\\\./, \"string.escape.invalid\"],\n      [/'/, { token: \"string\", next: \"@pop\" }],\n    ],\n    yaml_constants: [[/\\b(null|~)\\b/, \"constant.language.null\"]],\n  },\n};\n\nmonaco.languages.register({ id: \"fancy_toml\" });\nmonaco.languages.setLanguageConfiguration(\"fancy_toml\", fancy_toml_conf);\nmonaco.languages.setMonarchTokensProvider(\"fancy_toml\", fancy_toml_language);\n"
  },
  {
    "path": "frontend/src/monaco/index.ts",
    "content": "import * as monaco from \"monaco-editor\";\nimport editorWorker from \"monaco-editor/esm/vs/editor/editor.worker?worker\";\nimport jsonWorker from \"monaco-editor/esm/vs/language/json/json.worker?worker\";\nimport cssWorker from \"monaco-editor/esm/vs/language/css/css.worker?worker\";\nimport htmlWorker from \"monaco-editor/esm/vs/language/html/html.worker?worker\";\nimport tsWorker from \"monaco-editor/esm/vs/language/typescript/ts.worker?worker\";\nimport yamlWorker from \"monaco-yaml/yaml.worker?worker\";\n\nself.MonacoEnvironment = {\n  getWorker(_, label) {\n    if (label === \"json\") {\n      return new jsonWorker();\n    }\n    if (label === \"css\" || label === \"scss\" || label === \"less\") {\n      return new cssWorker();\n    }\n    if (label === \"html\" || label === \"handlebars\" || label === \"razor\") {\n      return new htmlWorker();\n    }\n    if (label === \"typescript\" || label === \"javascript\") {\n      return new tsWorker();\n    }\n    if (label === \"yaml\") {\n      return new yamlWorker();\n    }\n    return new editorWorker();\n  },\n};\n\nimport { loader } from \"@monaco-editor/react\";\nloader.config({ monaco });\n\n// Load the themes\nimport \"./theme\";\n// Load the parsers\nimport \"./yaml\";\nimport \"./toml\";\nimport \"./fancy_toml\";\nimport \"./shell\";\nimport \"./key_value\";\nimport \"./string_list\";\n"
  },
  {
    "path": "frontend/src/monaco/init.ts",
    "content": "import * as monaco from \"monaco-editor\";\n\nexport async function init_monaco() {\n  const promises = [\"lib\", \"responses\", \"types\", \"terminal\"].map((file) =>\n    Promise.all(\n      [\".js\", \".d.ts\"].map((extension) =>\n        fetch(`/client/${file}${extension}`)\n          .then((res) => res.text())\n          .then((dts) =>\n            monaco.languages.typescript.typescriptDefaults.addExtraLib(\n              dts,\n              `file:///client/${file}${extension}`\n            )\n          )\n      )\n    )\n  );\n  promises.push(\n    Promise.all(\n      [\"index.d.ts\", \"deno.d.ts\"].map((file) =>\n        fetch(`/${file}`)\n          .then((res) => res.text())\n          .then((dts) =>\n            monaco.languages.typescript.typescriptDefaults.addExtraLib(\n              dts,\n              `file:///${file}`\n            )\n          )\n      )\n    )\n  );\n\n  await Promise.all(promises);\n\n  type ExtraOptions = {\n    allowTopLevelAwait?: boolean;\n    moduleDetection?: \"force\" | \"auto\" | \"legacy\" | 3 | 2 | 1; // string or numeric enum\n  };\n\n  monaco.languages.typescript.typescriptDefaults.setCompilerOptions({\n    module: monaco.languages.typescript.ModuleKind.ESNext,\n    target: monaco.languages.typescript.ScriptTarget.ESNext,\n    allowNonTsExtensions: true,\n    moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,\n    typeRoots: [\"index.d.ts\"],\n    allowTopLevelAwait: true,\n    moduleDetection: \"force\",\n  } as monaco.languages.typescript.CompilerOptions & ExtraOptions);\n\n  monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({\n    diagnosticCodesToIgnore: [\n      // Allows top level await\n      1375,\n      // Allows top level return\n      1108,\n    ],\n  });\n}\n"
  },
  {
    "path": "frontend/src/monaco/key_value.ts",
    "content": "import * as monaco from \"monaco-editor\";\n\n// Language Configuration\nconst key_value_conf: monaco.languages.LanguageConfiguration = {\n  comments: {\n    lineComment: \"#\",\n  },\n  brackets: [],\n  autoClosingPairs: [\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n  ],\n  surroundingPairs: [\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n  ],\n};\n\n// Language Definition (Monarch Tokenizer)\nconst key_value_language = <monaco.languages.IMonarchLanguage>{\n  defaultToken: \"\",\n  tokenPostfix: \".env\",\n\n  escapes:\n    /\\\\(?:[abfnrtv\\\\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,\n\n  tokenizer: {\n    root: [\n      // Handle environment variables (KEY = VALUE or KEY: VALUE)\n      [\n        /(\\s*-*\\s*)([A-Za-z0-9_]+)(\\s*)(=|:)(\\s*)/,\n        [\n          \"\", // Optional leading hyphen\n          \"key\", // Key (environment variable)\n          \"\", // Whitespace\n          \"operator.assignment\", // Equals sign (=) or colon (:)\n          \"\", // Whitespace\n        ],\n      ],\n\n      // Parse value as yaml\n      { include: \"@yaml_whitespace\" },\n      { include: \"@yaml_comments\" },\n      { include: \"@yaml_keys\" },\n      { include: \"@yaml_numbers\" },\n      { include: \"@yaml_booleans\" },\n      { include: \"@yaml_strings\" },\n      { include: \"@yaml_constants\" },\n    ],\n\n    yaml_whitespace: [[/[ \\t\\r\\n]+/, \"\"]],\n\n    yaml_comments: [[/#.*$/, \"comment\"]],\n\n    yaml_keys: [[/([^\\s\\[\\]{},\"']+)(\\s*)(:)/, [\"key\", \"\", \"delimiter\"]]],\n\n    yaml_numbers: [\n      [/\\b\\d+\\.\\d*\\b/, \"number.float\"],\n      [/\\b0x[0-9a-fA-F]+\\b/, \"number.hex\"],\n      [/\\b\\d+\\b/, \"number\"],\n    ],\n\n    yaml_booleans: [\n      [/\\b(true|false|yes|no|on|off)\\b/, \"constant.language.boolean\"],\n    ],\n\n    yaml_strings: [\n      [/\"([^\"\\\\]|\\\\.)*$/, \"string.invalid\"], // non-terminated string\n      [/'([^'\\\\]|\\\\.)*$/, \"string.invalid\"], // non-terminated string\n      [/\"/, \"string\", \"@yaml_string_double\"],\n      [/'/, \"string\", \"@yaml_string_single\"],\n    ],\n\n    yaml_string_double: [\n      [/[^\\\\\"]+/, \"string\"],\n      [/@escapes/, \"string.escape\"],\n      [/\\\\./, \"string.escape.invalid\"],\n      [/\"/, \"string\", \"@pop\"],\n    ],\n\n    yaml_string_single: [\n      [/[^\\\\']+/, \"string\"],\n      [/@escapes/, \"string.escape\"],\n      [/\\\\./, \"string.escape.invalid\"],\n      [/'/, \"string\", \"@pop\"],\n    ],\n\n    yaml_constants: [[/\\b(null|~)\\b/, \"constant.language.null\"]],\n  },\n};\n\n// Register the new language\nmonaco.languages.register({ id: \"key_value\" });\n\n// Set the language configuration and tokenizer\nmonaco.languages.setLanguageConfiguration(\"key_value\", key_value_conf);\nmonaco.languages.setMonarchTokensProvider(\"key_value\", key_value_language);\n"
  },
  {
    "path": "frontend/src/monaco/shell.ts",
    "content": "import * as monaco from \"monaco-editor\";\n\nconst shell_conf: monaco.languages.LanguageConfiguration = {\n  comments: {\n    lineComment: \"#\",\n  },\n  brackets: [\n    [\"{\", \"}\"],\n    [\"[\", \"]\"],\n    [\"(\", \")\"],\n  ],\n  autoClosingPairs: [\n    { open: \"{\", close: \"}\" },\n    { open: \"[\", close: \"]\" },\n    { open: \"(\", close: \")\" },\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n    { open: \"`\", close: \"`\" },\n  ],\n  surroundingPairs: [\n    { open: \"{\", close: \"}\" },\n    { open: \"[\", close: \"]\" },\n    { open: \"(\", close: \")\" },\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n    { open: \"`\", close: \"`\" },\n  ],\n};\n\nconst shell_language = <monaco.languages.IMonarchLanguage>{\n  defaultToken: \"\",\n  ignoreCase: true,\n  tokenPostfix: \".shell\",\n\n  brackets: [\n    { token: \"delimiter.bracket\", open: \"{\", close: \"}\" },\n    { token: \"delimiter.parenthesis\", open: \"(\", close: \")\" },\n    { token: \"delimiter.square\", open: \"[\", close: \"]\" },\n  ],\n\n  keywords: [\n    \"if\",\n    \"then\",\n    \"do\",\n    \"else\",\n    \"elif\",\n    \"while\",\n    \"until\",\n    \"for\",\n    \"in\",\n    \"esac\",\n    \"fi\",\n    \"fin\",\n    \"fil\",\n    \"done\",\n    \"exit\",\n    \"set\",\n    \"unset\",\n    \"export\",\n    \"function\",\n  ],\n\n  builtins: [\n    \"ab\",\n    \"awk\",\n    \"bash\",\n    \"beep\",\n    \"cat\",\n    \"cc\",\n    \"cd\",\n    \"chown\",\n    \"chmod\",\n    \"chroot\",\n    \"clear\",\n    \"cp\",\n    \"curl\",\n    \"cut\",\n    \"diff\",\n    \"echo\",\n    \"find\",\n    \"gawk\",\n    \"gcc\",\n    \"get\",\n    \"git\",\n    \"grep\",\n    \"hg\",\n    \"kill\",\n    \"killall\",\n    \"ln\",\n    \"ls\",\n    \"make\",\n    \"mkdir\",\n    \"openssl\",\n    \"mv\",\n    \"nc\",\n    \"node\",\n    \"npm\",\n    \"ping\",\n    \"ps\",\n    \"restart\",\n    \"rm\",\n    \"rmdir\",\n    \"sed\",\n    \"service\",\n    \"sh\",\n    \"shopt\",\n    \"shred\",\n    \"source\",\n    \"sort\",\n    \"sleep\",\n    \"ssh\",\n    \"start\",\n    \"stop\",\n    \"su\",\n    \"sudo\",\n    \"svn\",\n    \"tee\",\n    \"telnet\",\n    \"top\",\n    \"touch\",\n    \"vi\",\n    \"vim\",\n    \"wall\",\n    \"wc\",\n    \"wget\",\n    \"who\",\n    \"write\",\n    \"yes\",\n    \"zsh\",\n  ],\n\n  startingWithDash: /\\-+\\w+/,\n\n  identifiersWithDashes: /[a-zA-Z]\\w+(?:@startingWithDash)+/,\n\n  // we include these common regular expressions\n  symbols: /[=><!~?&|+\\-*\\/\\^;\\.,]+/,\n\n  // The main tokenizer for our languages\n  tokenizer: {\n    root: [\n      [/@identifiersWithDashes/, \"\"],\n\n      [/(\\s)((?:@startingWithDash)+)/, [\"white\", \"attribute.name\"]],\n\n      [\n        /[a-zA-Z]\\w*/,\n        {\n          cases: {\n            \"@keywords\": \"keyword\",\n            \"@builtins\": \"type.identifier\",\n            \"@default\": \"\",\n          },\n        },\n      ],\n\n      { include: \"@whitespace\" },\n\n      { include: \"@strings\" },\n      { include: \"@parameters\" },\n      { include: \"@heredoc\" },\n\n      [/[{}\\[\\]()]/, \"@brackets\"],\n\n      [/@symbols/, \"delimiter\"],\n\n      { include: \"@numbers\" },\n\n      [/[,;]/, \"delimiter\"],\n    ],\n\n    whitespace: [\n      [/\\s+/, \"white\"],\n      [/(^#!.*$)/, \"metatag\"],\n      [/(^#.*$)/, \"comment\"],\n    ],\n\n    numbers: [\n      [/\\d*\\.\\d+([eE][\\-+]?\\d+)?/, \"number.float\"],\n      [/0[xX][0-9a-fA-F_]*[0-9a-fA-F]/, \"number.hex\"],\n      [/\\d+/, \"number\"],\n    ],\n\n    // Recognize strings, including those broken across lines\n    strings: [\n      [/'/, \"string\", \"@stringBody\"],\n      [/\"/, \"string\", \"@dblStringBody\"],\n    ],\n    stringBody: [\n      [/'/, \"string\", \"@popall\"],\n      [/./, \"string\"],\n    ],\n    dblStringBody: [\n      [/\"/, \"string\", \"@popall\"],\n      [/./, \"string\"],\n    ],\n\n    heredoc: [\n      [\n        /(<<[-<]?)(\\s*)(['\"`]?)([\\w\\-]+)(['\"`]?)/,\n        [\n          \"constants\",\n          \"white\",\n          \"string.heredoc.delimiter\",\n          \"string.heredoc\",\n          \"string.heredoc.delimiter\",\n        ],\n      ],\n    ],\n\n    parameters: [\n      [/\\$\\d+/, \"variable.predefined\"],\n      [/\\$\\w+/, \"variable\"],\n      [/\\$[*@#?\\-$!0_]/, \"variable\"],\n      [/\\$'/, \"variable\", \"@parameterBodyQuote\"],\n      [/\\$\"/, \"variable\", \"@parameterBodyDoubleQuote\"],\n      [/\\$\\(/, \"variable\", \"@parameterBodyParen\"],\n      [/\\$\\{/, \"variable\", \"@parameterBodyCurlyBrace\"],\n    ],\n    parameterBodyQuote: [\n      [/[^#:%*@\\-!_']+/, \"variable\"],\n      [/[#:%*@\\-!_]/, \"delimiter\"],\n      [/[']/, \"variable\", \"@pop\"],\n    ],\n    parameterBodyDoubleQuote: [\n      [/[^#:%*@\\-!_\"]+/, \"variable\"],\n      [/[#:%*@\\-!_]/, \"delimiter\"],\n      [/[\"]/, \"variable\", \"@pop\"],\n    ],\n    parameterBodyParen: [\n      [/[^#:%*@\\-!_)]+/, \"variable\"],\n      [/[#:%*@\\-!_]/, \"delimiter\"],\n      [/[)]/, \"variable\", \"@pop\"],\n    ],\n    parameterBodyCurlyBrace: [\n      [/[^#:%*@\\-!_}]+/, \"variable\"],\n      [/[#:%*@\\-!_]/, \"delimiter\"],\n      [/[}]/, \"variable\", \"@pop\"],\n    ],\n  },\n};\n\n// Register the shell language\nmonaco.languages.register({ id: \"shell\" });\nmonaco.languages.setLanguageConfiguration(\"shell\", shell_conf);\nmonaco.languages.setMonarchTokensProvider(\"shell\", shell_language);\n"
  },
  {
    "path": "frontend/src/monaco/string_list.ts",
    "content": "import * as monaco from \"monaco-editor\";\n\nconst string_list_conf: monaco.languages.LanguageConfiguration = {\n  comments: {\n    lineComment: \"#\",\n  },\n  autoClosingPairs: [\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n  ],\n  surroundingPairs: [\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n  ],\n};\n\nconst string_list_language = <monaco.languages.IMonarchLanguage>{\n  defaultToken: \"\",\n  tokenPostfix: \".string_list\",\n\n  tokenizer: {\n    root: [\n      // Comments\n      [/#.*$/, \"comment\"],\n\n      // Comma as a delimiter\n      [/,/, \"comment\"],\n      [/\\*/, \"keyword\"],\n      [/\\?/, \"keyword\"],\n\n      // Special syntax: text surrounded by \\\n      // [/\\\\[^\\\\]*\\\\/, \"keyword\"],\n      [/\\\\/, { token: \"keyword\", next: \"@regex\" }],\n\n      // Main strings separated by spaces or newlines\n      [/[^\\*\\?,#\\\\\\s]+/, \"\"],\n\n      // Whitespace\n      [/[ \\t\\r\\n]+/, \"\"],\n    ],\n    regex: [\n      // Regex tokens\n      [/\\[[^\\]]*\\]/, \"\"], // Character classes like [abc]\n      [/[*+?\\.]+/, \"keyword\"], // Quantifiers like *, +, ?\n      [/\\\\./, \"string.regexp constant.character.escape\"], // Escape sequences like \\d, \\w\n      [/[^\\\\]/, \"string\"], // Any other regex content\n      [/\\\\/, { token: \"keyword\", next: \"@pop\" }], // Closing backslash returns to root\n    ],\n  },\n};\n\n// Register the custom language and configuration with Monaco\nmonaco.languages.register({ id: \"string_list\" });\nmonaco.languages.setLanguageConfiguration(\"string_list\", string_list_conf);\nmonaco.languages.setMonarchTokensProvider(\"string_list\", string_list_language);\n"
  },
  {
    "path": "frontend/src/monaco/theme.ts",
    "content": "import * as monaco from \"monaco-editor\";\n\n// background: #f7f8f9\nmonaco.editor.defineTheme(\"light\", {\n  base: \"vs\",\n  inherit: true,\n  rules: [\n    {\n      background: \"f7f8f9\",\n      token: \"\",\n    },\n    {\n      foreground: \"6a737d\",\n      token: \"comment\",\n    },\n    {\n      foreground: \"6a737d\",\n      token: \"punctuation.definition.comment\",\n    },\n    {\n      foreground: \"6a737d\",\n      token: \"string.comment\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"constant\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"entity.name.constant\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"variable.other.constant\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"variable.language\",\n    },\n    {\n      foreground: \"6f42c1\",\n      token: \"entity\",\n    },\n    {\n      foreground: \"6f42c1\",\n      token: \"entity.name\",\n    },\n    {\n      foreground: \"24292e\",\n      token: \"variable.parameter.function\",\n    },\n    {\n      foreground: \"22863a\",\n      token: \"entity.name.tag\",\n    },\n    {\n      foreground: \"d73a49\",\n      token: \"keyword\",\n    },\n    {\n      foreground: \"d73a49\",\n      token: \"storage\",\n    },\n    {\n      foreground: \"d73a49\",\n      token: \"storage.type\",\n    },\n    {\n      foreground: \"24292e\",\n      token: \"storage.modifier.package\",\n    },\n    {\n      foreground: \"24292e\",\n      token: \"storage.modifier.import\",\n    },\n    {\n      foreground: \"24292e\",\n      token: \"storage.type.java\",\n    },\n    {\n      foreground: \"032f62\",\n      token: \"string\",\n    },\n    {\n      foreground: \"032f62\",\n      token: \"punctuation.definition.string\",\n    },\n    {\n      foreground: \"032f62\",\n      token: \"string punctuation.section.embedded source\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"support\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"meta.property-name\",\n    },\n    {\n      foreground: \"e36209\",\n      token: \"variable\",\n    },\n    {\n      foreground: \"24292e\",\n      token: \"variable.other\",\n    },\n    {\n      foreground: \"b31d28\",\n      fontStyle: \"bold italic underline\",\n      token: \"invalid.broken\",\n    },\n    {\n      foreground: \"b31d28\",\n      fontStyle: \"bold italic underline\",\n      token: \"invalid.deprecated\",\n    },\n    {\n      foreground: \"fafbfc\",\n      background: \"b31d28\",\n      fontStyle: \"italic underline\",\n      token: \"invalid.illegal\",\n    },\n    {\n      foreground: \"fafbfc\",\n      background: \"d73a49\",\n      fontStyle: \"italic underline\",\n      token: \"carriage-return\",\n    },\n    {\n      foreground: \"b31d28\",\n      fontStyle: \"bold italic underline\",\n      token: \"invalid.unimplemented\",\n    },\n    {\n      foreground: \"b31d28\",\n      token: \"message.error\",\n    },\n    {\n      foreground: \"24292e\",\n      token: \"string source\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"string variable\",\n    },\n    {\n      foreground: \"032f62\",\n      token: \"source.regexp\",\n    },\n    {\n      foreground: \"032f62\",\n      token: \"string.regexp\",\n    },\n    {\n      foreground: \"032f62\",\n      token: \"string.regexp.character-class\",\n    },\n    {\n      foreground: \"032f62\",\n      token: \"string.regexp constant.character.escape\",\n    },\n    {\n      foreground: \"032f62\",\n      token: \"string.regexp source.ruby.embedded\",\n    },\n    {\n      foreground: \"032f62\",\n      token: \"string.regexp string.regexp.arbitrary-repitition\",\n    },\n    {\n      foreground: \"22863a\",\n      fontStyle: \"bold\",\n      token: \"string.regexp constant.character.escape\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"support.constant\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"support.variable\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"meta.module-reference\",\n    },\n    {\n      foreground: \"735c0f\",\n      token: \"markup.list\",\n    },\n    {\n      foreground: \"005cc5\",\n      fontStyle: \"bold\",\n      token: \"markup.heading\",\n    },\n    {\n      foreground: \"005cc5\",\n      fontStyle: \"bold\",\n      token: \"markup.heading entity.name\",\n    },\n    {\n      foreground: \"22863a\",\n      token: \"markup.quote\",\n    },\n    {\n      foreground: \"24292e\",\n      fontStyle: \"italic\",\n      token: \"markup.italic\",\n    },\n    {\n      foreground: \"24292e\",\n      fontStyle: \"bold\",\n      token: \"markup.bold\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"markup.raw\",\n    },\n    {\n      foreground: \"b31d28\",\n      background: \"ffeef0\",\n      token: \"markup.deleted\",\n    },\n    {\n      foreground: \"b31d28\",\n      background: \"ffeef0\",\n      token: \"meta.diff.header.from-file\",\n    },\n    {\n      foreground: \"b31d28\",\n      background: \"ffeef0\",\n      token: \"punctuation.definition.deleted\",\n    },\n    {\n      foreground: \"22863a\",\n      background: \"f0fff4\",\n      token: \"markup.inserted\",\n    },\n    {\n      foreground: \"22863a\",\n      background: \"f0fff4\",\n      token: \"meta.diff.header.to-file\",\n    },\n    {\n      foreground: \"22863a\",\n      background: \"f0fff4\",\n      token: \"punctuation.definition.inserted\",\n    },\n    {\n      foreground: \"e36209\",\n      background: \"ffebda\",\n      token: \"markup.changed\",\n    },\n    {\n      foreground: \"e36209\",\n      background: \"ffebda\",\n      token: \"punctuation.definition.changed\",\n    },\n    {\n      foreground: \"f6f8fa\",\n      background: \"005cc5\",\n      token: \"markup.ignored\",\n    },\n    {\n      foreground: \"f6f8fa\",\n      background: \"005cc5\",\n      token: \"markup.untracked\",\n    },\n    {\n      foreground: \"6f42c1\",\n      fontStyle: \"bold\",\n      token: \"meta.diff.range\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"meta.diff.header\",\n    },\n    {\n      foreground: \"005cc5\",\n      fontStyle: \"bold\",\n      token: \"meta.separator\",\n    },\n    {\n      foreground: \"005cc5\",\n      token: \"meta.output\",\n    },\n    {\n      foreground: \"586069\",\n      token: \"brackethighlighter.tag\",\n    },\n    {\n      foreground: \"586069\",\n      token: \"brackethighlighter.curly\",\n    },\n    {\n      foreground: \"586069\",\n      token: \"brackethighlighter.round\",\n    },\n    {\n      foreground: \"586069\",\n      token: \"brackethighlighter.square\",\n    },\n    {\n      foreground: \"586069\",\n      token: \"brackethighlighter.angle\",\n    },\n    {\n      foreground: \"586069\",\n      token: \"brackethighlighter.quote\",\n    },\n    {\n      foreground: \"b31d28\",\n      token: \"brackethighlighter.unmatched\",\n    },\n    {\n      foreground: \"b31d28\",\n      token: \"sublimelinter.mark.error\",\n    },\n    {\n      foreground: \"e36209\",\n      token: \"sublimelinter.mark.warning\",\n    },\n    {\n      foreground: \"959da5\",\n      token: \"sublimelinter.gutter-mark\",\n    },\n    {\n      foreground: \"032f62\",\n      fontStyle: \"underline\",\n      token: \"constant.other.reference.link\",\n    },\n    {\n      foreground: \"032f62\",\n      fontStyle: \"underline\",\n      token: \"string.other.link\",\n    },\n  ],\n  colors: {\n    \"editor.background\": \"#f7f8f9\",\n    \"editor.foreground\": \"#24292e\",\n    // \"editor.selectionBackground\": \"#c8c8fa\",\n    // \"editor.inactiveSelectionBackground\": \"#fafbfc\",\n    // \"editor.lineHighlightBackground\": \"#fafbfc\",\n    \"editorCursor.foreground\": \"#24292e\",\n    \"editorWhitespace.foreground\": \"#959da5\",\n    \"editorIndentGuide.background\": \"#959da5\",\n    \"editorIndentGuide.activeBackground\": \"#24292e\",\n    \"editor.selectionHighlightBorder\": \"#fafbfc\",\n  },\n});\n\n// background: 151b25\nmonaco.editor.defineTheme(\"dark\", {\n  base: \"vs-dark\",\n  inherit: true,\n  rules: [\n    {\n      background: \"151b25\",\n      token: \"\",\n    },\n    {\n      foreground: \"959da5\",\n      token: \"comment\",\n    },\n    {\n      foreground: \"959da5\",\n      token: \"punctuation.definition.comment\",\n    },\n    {\n      foreground: \"959da5\",\n      token: \"string.comment\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"constant\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"entity.name.constant\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"variable.other.constant\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"variable.language\",\n    },\n    {\n      foreground: \"b392f0\",\n      token: \"entity\",\n    },\n    {\n      foreground: \"b392f0\",\n      token: \"entity.name\",\n    },\n    {\n      foreground: \"f6f8fa\",\n      token: \"variable.parameter.function\",\n    },\n    {\n      foreground: \"7bcc72\",\n      token: \"entity.name.tag\",\n    },\n    {\n      foreground: \"ea4a5a\",\n      token: \"keyword\",\n    },\n    {\n      foreground: \"ea4a5a\",\n      token: \"storage\",\n    },\n    {\n      foreground: \"ea4a5a\",\n      token: \"storage.type\",\n    },\n    {\n      foreground: \"f6f8fa\",\n      token: \"storage.modifier.package\",\n    },\n    {\n      foreground: \"f6f8fa\",\n      token: \"storage.modifier.import\",\n    },\n    {\n      foreground: \"f6f8fa\",\n      token: \"storage.type.java\",\n    },\n    {\n      foreground: \"79b8ff\",\n      token: \"string\",\n    },\n    {\n      foreground: \"79b8ff\",\n      token: \"punctuation.definition.string\",\n    },\n    {\n      foreground: \"79b8ff\",\n      token: \"string punctuation.section.embedded source\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"support\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"meta.property-name\",\n    },\n    {\n      foreground: \"fb8532\",\n      token: \"variable\",\n    },\n    {\n      foreground: \"f6f8fa\",\n      token: \"variable.other\",\n    },\n    {\n      foreground: \"d73a49\",\n      fontStyle: \"bold italic underline\",\n      token: \"invalid.broken\",\n    },\n    {\n      foreground: \"d73a49\",\n      fontStyle: \"bold italic underline\",\n      token: \"invalid.deprecated\",\n    },\n    {\n      foreground: \"fafbfc\",\n      background: \"d73a49\",\n      fontStyle: \"italic underline\",\n      token: \"invalid.illegal\",\n    },\n    {\n      foreground: \"fafbfc\",\n      background: \"d73a49\",\n      fontStyle: \"italic underline\",\n      token: \"carriage-return\",\n    },\n    {\n      foreground: \"d73a49\",\n      fontStyle: \"bold italic underline\",\n      token: \"invalid.unimplemented\",\n    },\n    {\n      foreground: \"d73a49\",\n      token: \"message.error\",\n    },\n    {\n      foreground: \"f6f8fa\",\n      token: \"string source\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"string variable\",\n    },\n    {\n      foreground: \"79b8ff\",\n      token: \"source.regexp\",\n    },\n    {\n      foreground: \"79b8ff\",\n      token: \"string.regexp\",\n    },\n    {\n      foreground: \"79b8ff\",\n      token: \"string.regexp.character-class\",\n    },\n    {\n      foreground: \"79b8ff\",\n      token: \"string.regexp constant.character.escape\",\n    },\n    {\n      foreground: \"79b8ff\",\n      token: \"string.regexp source.ruby.embedded\",\n    },\n    {\n      foreground: \"79b8ff\",\n      token: \"string.regexp string.regexp.arbitrary-repitition\",\n    },\n    {\n      foreground: \"7bcc72\",\n      fontStyle: \"bold\",\n      token: \"string.regexp constant.character.escape\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"support.constant\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"support.variable\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"meta.module-reference\",\n    },\n    {\n      foreground: \"fb8532\",\n      token: \"markup.list\",\n    },\n    {\n      foreground: \"0366d6\",\n      fontStyle: \"bold\",\n      token: \"markup.heading\",\n    },\n    {\n      foreground: \"0366d6\",\n      fontStyle: \"bold\",\n      token: \"markup.heading entity.name\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"markup.quote\",\n    },\n    {\n      foreground: \"f6f8fa\",\n      fontStyle: \"italic\",\n      token: \"markup.italic\",\n    },\n    {\n      foreground: \"f6f8fa\",\n      fontStyle: \"bold\",\n      token: \"markup.bold\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"markup.raw\",\n    },\n    {\n      foreground: \"b31d28\",\n      background: \"ffeef0\",\n      token: \"markup.deleted\",\n    },\n    {\n      foreground: \"b31d28\",\n      background: \"ffeef0\",\n      token: \"meta.diff.header.from-file\",\n    },\n    {\n      foreground: \"b31d28\",\n      background: \"ffeef0\",\n      token: \"punctuation.definition.deleted\",\n    },\n    {\n      foreground: \"176f2c\",\n      background: \"f0fff4\",\n      token: \"markup.inserted\",\n    },\n    {\n      foreground: \"176f2c\",\n      background: \"f0fff4\",\n      token: \"meta.diff.header.to-file\",\n    },\n    {\n      foreground: \"176f2c\",\n      background: \"f0fff4\",\n      token: \"punctuation.definition.inserted\",\n    },\n    {\n      foreground: \"b08800\",\n      background: \"fffdef\",\n      token: \"markup.changed\",\n    },\n    {\n      foreground: \"b08800\",\n      background: \"fffdef\",\n      token: \"punctuation.definition.changed\",\n    },\n    {\n      foreground: \"2f363d\",\n      background: \"959da5\",\n      token: \"markup.ignored\",\n    },\n    {\n      foreground: \"2f363d\",\n      background: \"959da5\",\n      token: \"markup.untracked\",\n    },\n    {\n      foreground: \"b392f0\",\n      fontStyle: \"bold\",\n      token: \"meta.diff.range\",\n    },\n    {\n      foreground: \"c8e1ff\",\n      token: \"meta.diff.header\",\n    },\n    {\n      foreground: \"0366d6\",\n      fontStyle: \"bold\",\n      token: \"meta.separator\",\n    },\n    {\n      foreground: \"0366d6\",\n      token: \"meta.output\",\n    },\n    {\n      foreground: \"ffeef0\",\n      token: \"brackethighlighter.tag\",\n    },\n    {\n      foreground: \"ffeef0\",\n      token: \"brackethighlighter.curly\",\n    },\n    {\n      foreground: \"ffeef0\",\n      token: \"brackethighlighter.round\",\n    },\n    {\n      foreground: \"ffeef0\",\n      token: \"brackethighlighter.square\",\n    },\n    {\n      foreground: \"ffeef0\",\n      token: \"brackethighlighter.angle\",\n    },\n    {\n      foreground: \"ffeef0\",\n      token: \"brackethighlighter.quote\",\n    },\n    {\n      foreground: \"d73a49\",\n      token: \"brackethighlighter.unmatched\",\n    },\n    {\n      foreground: \"d73a49\",\n      token: \"sublimelinter.mark.error\",\n    },\n    {\n      foreground: \"fb8532\",\n      token: \"sublimelinter.mark.warning\",\n    },\n    {\n      foreground: \"6a737d\",\n      token: \"sublimelinter.gutter-mark\",\n    },\n    {\n      foreground: \"79b8ff\",\n      fontStyle: \"underline\",\n      token: \"constant.other.reference.link\",\n    },\n    {\n      foreground: \"79b8ff\",\n      fontStyle: \"underline\",\n      token: \"string.other.link\",\n    },\n  ],\n  colors: {\n    \"editor.background\": \"#151b25\",\n    \"editor.foreground\": \"#f6f8fa\",\n    // \"editor.selectionBackground\": \"#4c2889\",\n    // \"editor.inactiveSelectionBackground\": \"#444d56\",\n    // \"editor.lineHighlightBackground\": \"#444d56\",\n    \"editorCursor.foreground\": \"#ffffff\",\n    \"editorWhitespace.foreground\": \"#6a737d\",\n    \"editorIndentGuide.background\": \"#6a737d\",\n    \"editorIndentGuide.activeBackground\": \"#f6f8fa\",\n    \"editor.selectionHighlightBorder\": \"#444d56\",\n  },\n});\n"
  },
  {
    "path": "frontend/src/monaco/toml.ts",
    "content": "import * as monaco from \"monaco-editor\";\n\n/* -------------------------------------------------\n *  Language configuration  (unchanged)\n * ------------------------------------------------- */\nconst toml_conf: monaco.languages.LanguageConfiguration = {\n  comments: { lineComment: \"#\" },\n  brackets: [\n    [\"{\", \"}\"],\n    [\"[\", \"]\"],\n    [\"(\", \")\"],\n  ],\n  autoClosingPairs: [\n    { open: \"{\", close: \"}\" },\n    { open: \"[\", close: \"]\" },\n    { open: \"(\", close: \")\" },\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n    { open: '\"\"\"', close: '\"\"\"' },\n  ],\n  surroundingPairs: [\n    { open: \"{\", close: \"}\" },\n    { open: \"[\", close: \"]\" },\n    { open: \"(\", close: \")\" },\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n    { open: '\"\"\"', close: '\"\"\"' },\n  ],\n};\n\n/* -------------------------------------------------\n *  Monarch tokenizer – TOML-only\n * ------------------------------------------------- */\nconst toml_language: monaco.languages.IMonarchLanguage = {\n  defaultToken: \"\",\n  tokenPostfix: \".toml\",\n\n  escapes: /\\\\(?:[btnfr\"'\\\\\\/]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,\n\n  tokenizer: {\n    /* ---------- root ---------- */\n    root: [\n      { include: \"@comments\" },\n\n      /* Tables & array-tables */\n      [\n        /^\\s*(\\[\\[)([^[\\]]+)(\\]\\])/,\n        [\n          \"punctuation.definition.array.table\",\n          \"entity.other.attribute-name.table.array\",\n          \"punctuation.definition.array.table\",\n        ],\n      ],\n      [\n        /^\\s*(\\[)([^[\\]]+)(\\])/,\n        [\n          \"punctuation.definition.table\",\n          \"entity.other.attribute-name.table\",\n          \"punctuation.definition.table\",\n        ],\n      ],\n\n      /* Inline tables */\n      [\n        /\\{/,\n        { token: \"punctuation.definition.table.inline\", next: \"@inlineTable\" },\n      ],\n\n      /* Key-value pair */\n      [\n        /\\s*((?:(?:(?:[A-Za-z0-9_+\\-]+)|(?:\\\"[^\\\"]+\\\")|(?:'[^']+'))\\s*\\.?\\s*)+)\\s*(=)/,\n        [\"\", \"delimiter\"],\n      ],\n\n      /* Values */\n      { include: \"@values\" },\n    ],\n\n    /* ---------- inline table ---------- */\n    inlineTable: [\n      [/\\}/, { token: \"punctuation.definition.table.inline\", next: \"@pop\" }],\n      { include: \"@comments\" },\n      [/,/, \"punctuation.separator.table.inline\"],\n      { include: \"@values\" },\n    ],\n\n    /* ---------- values ---------- */\n    values: [\n      /* Strings ---------------------------------------------------- */\n      [/\"\"\"/, { token: \"string\", next: \"@tripleBasicString\" }],\n      [/\"/, { token: \"string\", next: \"@basicString\" }],\n      [/'''/, { token: \"string\", next: \"@tripleLiteralString\" }],\n      [/'/, { token: \"string\", next: \"@literalStringSingle\" }],\n\n      /* Dates, times, booleans ------------------------------------ */\n      [\n        /\\d{4}-\\d{2}-\\d{2}[Tt ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})/,\n        \"constant.other.time.datetime.offset\",\n      ],\n      [\n        /\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?/,\n        \"constant.other.time.datetime.local\",\n      ],\n      [/\\d{4}-\\d{2}-\\d{2}/, \"constant.other.time.date\"],\n      [/\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?/, \"constant.other.time.time\"],\n      [/\\b(true|false)\\b/, \"constant.language.boolean\"],\n\n      /* Numbers ---------------------------------------------------- */\n      [/[+-]?(0x[0-9A-Fa-f_]+|0o[0-7_]+|0b[01_]+)/, \"number.hex\"],\n      [\n        /[+-]?(?:\\d(?:_?\\d)*)(?:\\.\\d(?:_?\\d)*)?(?:[eE][+-]?\\d(?:_?\\d)*)?/,\n        \"number.float\",\n      ],\n      [/[+-]?\\d(?:_?\\d)*/, \"number\"],\n\n      /* Arrays ----------------------------------------------------- */\n      [/\\[/, { token: \"punctuation.definition.array\", next: \"@array\" }],\n    ],\n\n    /* ---------- arrays ---------- */\n    array: [\n      [/\\]/, { token: \"punctuation.definition.array\", next: \"@pop\" }],\n      [/,/, \"punctuation.separator.array\"],\n      { include: \"@values\" },\n    ],\n\n    /* ---------- strings ---------- */\n    basicString: [\n      [/[^\\\\\"]+/, \"string\"],\n      [/@escapes/, \"string.escape\"],\n      [/\\\\./, \"invalid\"],\n      [/\"/, { token: \"string\", next: \"@pop\" }],\n    ],\n\n    tripleBasicString: [\n      [/\"\"\"/, { token: \"string\", next: \"@pop\" }],\n      [/[^\\\\\"]+/, \"string\"],\n      [/@escapes/, \"string.escape\"],\n      [/\\\\./, \"string.invalid\"],\n    ],\n\n    literalStringSingle: [\n      [/[^']+/, \"string\"],\n      [/'/, { token: \"string\", next: \"@pop\" }],\n    ],\n\n    tripleLiteralString: [\n      [/'''/, { token: \"string\", next: \"@pop\" }],\n      [/[^']+/, \"string\"],\n    ],\n\n    /* ---------- misc helpers ---------- */\n    comments: [[/\\s*((#).*)$/, \"comment\"]],\n  },\n};\n\n/* -------------------------------------------------\n *  Register with Monaco\n * ------------------------------------------------- */\nmonaco.languages.register({ id: \"toml\" });\nmonaco.languages.setLanguageConfiguration(\"toml\", toml_conf);\nmonaco.languages.setMonarchTokensProvider(\"toml\", toml_language);\n"
  },
  {
    "path": "frontend/src/monaco/yaml.ts",
    "content": "import * as monaco from \"monaco-editor\";\nimport { configureMonacoYaml } from \"monaco-yaml\";\n\n// This is the one provided by Microsoft.\n// https://github.com/microsoft/monaco-editor/blob/main/src/basic-languages/yaml/yaml.ts\nconst yaml_conf: monaco.languages.LanguageConfiguration = {\n  comments: {\n    lineComment: \"#\",\n  },\n  brackets: [\n    [\"{\", \"}\"],\n    [\"[\", \"]\"],\n    [\"(\", \")\"],\n  ],\n  autoClosingPairs: [\n    { open: \"{\", close: \"}\" },\n    { open: \"[\", close: \"]\" },\n    { open: \"(\", close: \")\" },\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n  ],\n  surroundingPairs: [\n    { open: \"{\", close: \"}\" },\n    { open: \"[\", close: \"]\" },\n    { open: \"(\", close: \")\" },\n    { open: '\"', close: '\"' },\n    { open: \"'\", close: \"'\" },\n  ],\n  folding: {\n    offSide: true,\n  },\n  onEnterRules: [\n    {\n      beforeText: /:\\s*$/,\n      action: {\n        indentAction: monaco.languages.IndentAction.Indent,\n      },\n    },\n  ],\n};\n\nconst yaml_language = <monaco.languages.IMonarchLanguage>{\n  tokenPostfix: \".yaml\",\n\n  brackets: [\n    { token: \"delimiter.bracket\", open: \"{\", close: \"}\" },\n    { token: \"delimiter.square\", open: \"[\", close: \"]\" },\n  ],\n\n  keywords: [\n    \"true\",\n    \"True\",\n    \"TRUE\",\n    \"false\",\n    \"False\",\n    \"FALSE\",\n    \"null\",\n    \"Null\",\n    \"Null\",\n    \"~\",\n  ],\n\n  numberInteger: /(?:0|[+-]?[0-9]+)/,\n  numberFloat: /(?:0|[+-]?[0-9]+)(?:\\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/,\n  numberOctal: /0o[0-7]+/,\n  numberHex: /0x[0-9a-fA-F]+/,\n  numberInfinity: /[+-]?\\.(?:inf|Inf|INF)/,\n  numberNaN: /\\.(?:nan|Nan|NAN)/,\n  numberDate:\n    /\\d{4}-\\d\\d-\\d\\d([Tt ]\\d\\d:\\d\\d:\\d\\d(\\.\\d+)?(( ?[+-]\\d\\d?(:\\d\\d)?)|Z)?)?/,\n\n  escapes: /\\\\(?:[btnfr\\\\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,\n\n  tokenizer: {\n    root: [\n      { include: \"@whitespace\" },\n      { include: \"@comment\" },\n\n      // Directive\n      [/%[^ ]+.*$/, \"meta.directive\"],\n\n      // Document Markers\n      [/---/, \"operators.directivesEnd\"],\n      [/\\.{3}/, \"operators.documentEnd\"],\n\n      // Block Structure Indicators\n      [/[-?:](?= )/, \"operators\"],\n\n      { include: \"@anchor\" },\n      { include: \"@tagHandle\" },\n      { include: \"@flowCollections\" },\n      { include: \"@blockStyle\" },\n\n      // Numbers\n      [/@numberInteger(?![ \\t]*\\S+)/, \"number\"],\n      [/@numberFloat(?![ \\t]*\\S+)/, \"number.float\"],\n      [/@numberOctal(?![ \\t]*\\S+)/, \"number.octal\"],\n      [/@numberHex(?![ \\t]*\\S+)/, \"number.hex\"],\n      [/@numberInfinity(?![ \\t]*\\S+)/, \"number.infinity\"],\n      [/@numberNaN(?![ \\t]*\\S+)/, \"number.nan\"],\n      [/@numberDate(?![ \\t]*\\S+)/, \"number.date\"],\n\n      // Key:Value pair\n      [\n        /(\".*?\"|'.*?'|[^#'\"]*?)([ \\t]*)(:)( |$)/,\n        [\"key\", \"white\", \"operators\", \"white\"],\n      ],\n\n      { include: \"@flowScalars\" },\n\n      // String nodes\n      [\n        /.+?(?=(\\s+#|$))/,\n        {\n          cases: {\n            \"@keywords\": \"keyword\",\n            \"@default\": \"string\",\n          },\n        },\n      ],\n    ],\n\n    // Flow Collection: Flow Mapping\n    object: [\n      { include: \"@whitespace\" },\n      { include: \"@comment\" },\n\n      // Flow Mapping termination\n      [/\\}/, \"@brackets\", \"@pop\"],\n\n      // Flow Mapping delimiter\n      [/,/, \"delimiter.comma\"],\n\n      // Flow Mapping Key:Value delimiter\n      [/:(?= )/, \"operators\"],\n\n      // Flow Mapping Key:Value key\n      [/(?:\".*?\"|'.*?'|[^,\\{\\[]+?)(?=: )/, \"type\"],\n\n      // Start Flow Style\n      { include: \"@flowCollections\" },\n      { include: \"@flowScalars\" },\n\n      // Scalar Data types\n      { include: \"@tagHandle\" },\n      { include: \"@anchor\" },\n      { include: \"@flowNumber\" },\n\n      // Other value (keyword or string)\n      [\n        /[^\\},]+/,\n        {\n          cases: {\n            \"@keywords\": \"keyword\",\n            \"@default\": \"string\",\n          },\n        },\n      ],\n    ],\n\n    // Flow Collection: Flow Sequence\n    array: [\n      { include: \"@whitespace\" },\n      { include: \"@comment\" },\n\n      // Flow Sequence termination\n      [/\\]/, \"@brackets\", \"@pop\"],\n\n      // Flow Sequence delimiter\n      [/,/, \"delimiter.comma\"],\n\n      // Start Flow Style\n      { include: \"@flowCollections\" },\n      { include: \"@flowScalars\" },\n\n      // Scalar Data types\n      { include: \"@tagHandle\" },\n      { include: \"@anchor\" },\n      { include: \"@flowNumber\" },\n\n      // Other value (keyword or string)\n      [\n        /[^\\],]+/,\n        {\n          cases: {\n            \"@keywords\": \"keyword\",\n            \"@default\": \"string\",\n          },\n        },\n      ],\n    ],\n\n    // First line of a Block Style\n    multiString: [[/^( +).+$/, \"string\", \"@multiStringContinued.$1\"]],\n\n    // Further lines of a Block Style\n    //   Workaround for indentation detection\n    multiStringContinued: [\n      [\n        /^( *).+$/,\n        {\n          cases: {\n            \"$1==$S2\": \"string\",\n            \"@default\": { token: \"@rematch\", next: \"@popall\" },\n          },\n        },\n      ],\n    ],\n\n    whitespace: [[/[ \\t\\r\\n]+/, \"white\"]],\n\n    // Only line comments\n    comment: [[/#.*$/, \"comment\"]],\n\n    // Start Flow Collections\n    flowCollections: [\n      [/\\[/, \"@brackets\", \"@array\"],\n      [/\\{/, \"@brackets\", \"@object\"],\n    ],\n\n    // Start Flow Scalars (quoted strings)\n    flowScalars: [\n      [/\"([^\"\\\\]|\\\\.)*$/, \"string.invalid\"],\n      [/'([^'\\\\]|\\\\.)*$/, \"string.invalid\"],\n      [/'[^']*'/, \"string\"],\n      [/\"/, \"string\", \"@doubleQuotedString\"],\n    ],\n\n    doubleQuotedString: [\n      [/[^\\\\\"]+/, \"string\"],\n      [/@escapes/, \"string.escape\"],\n      [/\\\\./, \"string.escape.invalid\"],\n      [/\"/, \"string\", \"@pop\"],\n    ],\n\n    // Start Block Scalar\n    blockStyle: [[/[>|][0-9]*[+-]?$/, \"operators\", \"@multiString\"]],\n\n    // Numbers in Flow Collections (terminate with ,]})\n    flowNumber: [\n      [/@numberInteger(?=[ \\t]*[,\\]\\}])/, \"number\"],\n      [/@numberFloat(?=[ \\t]*[,\\]\\}])/, \"number.float\"],\n      [/@numberOctal(?=[ \\t]*[,\\]\\}])/, \"number.octal\"],\n      [/@numberHex(?=[ \\t]*[,\\]\\}])/, \"number.hex\"],\n      [/@numberInfinity(?=[ \\t]*[,\\]\\}])/, \"number.infinity\"],\n      [/@numberNaN(?=[ \\t]*[,\\]\\}])/, \"number.nan\"],\n      [/@numberDate(?=[ \\t]*[,\\]\\}])/, \"number.date\"],\n    ],\n\n    tagHandle: [[/\\![^ ]*/, \"tag\"]],\n\n    anchor: [[/[&*][^ ]+/, \"namespace\"]],\n  },\n};\n\nconfigureMonacoYaml(monaco, {\n  enableSchemaRequest: true,\n  schemas: [\n    {\n      fileMatch: [\"**/*compose.yml\", \"**/*compose.yaml\"],\n      uri: new URL(\n        \"/schema/compose-spec.json\",\n        window.location.href\n      ).toString(),\n    },\n  ],\n});\n\nmonaco.languages.register({ id: \"yaml\", aliases: [\"yml\"] });\nmonaco.languages.setMonarchTokensProvider(\"yaml\", yaml_language);\nmonaco.languages.setLanguageConfiguration(\"yaml\", yaml_conf);\n\n/// V1\n// const yaml_conf: monaco.languages.LanguageConfiguration = {\n//   comments: {\n//     lineComment: \"#\",\n//   },\n//   brackets: [\n//     [\"{\", \"}\"],\n//     [\"[\", \"]\"],\n//   ],\n//   autoClosingPairs: [\n//     { open: \"{\", close: \"}\" },\n//     { open: \"[\", close: \"]\" },\n//     { open: '\"', close: '\"' },\n//     { open: \"'\", close: \"'\" },\n//   ],\n//   surroundingPairs: [\n//     { open: \"{\", close: \"}\" },\n//     { open: \"[\", close: \"]\" },\n//     { open: '\"', close: '\"' },\n//     { open: \"'\", close: \"'\" },\n//   ],\n//   indentationRules: {\n//     increaseIndentPattern: /^.*(\\{[^}]*|\\[[^\\]]*)$/,\n//     decreaseIndentPattern: /^\\s*[}\\]],?\\s*$/,\n//   },\n// };\n\n// const yaml_language = <monaco.languages.IMonarchLanguage>{\n//   defaultToken: \"\",\n//   tokenPostfix: \".yaml\",\n\n//   // Common regular expressions\n//   escapes:\n//     /\\\\(?:[abfnrtv\\\\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,\n\n//   // The main tokenizer for YAML\n//   tokenizer: {\n//     root: [\n//       { include: \"@whitespace\" },\n//       { include: \"@comments\" },\n//       { include: \"@keys\" },\n//       { include: \"@numbers\" },\n//       { include: \"@booleans\" },\n//       { include: \"@strings\" },\n//       { include: \"@constants\" },\n//     ],\n\n//     whitespace: [[/[ \\t\\r\\n]+/, \"\"]],\n\n//     comments: [[/#.*$/, \"comment\"]],\n\n//     keys: [[/([^\\s\\[\\]{},\"']+)(\\s*)(:)/, [\"key\", \"\", \"delimiter\"]]],\n\n//     numbers: [\n//       [/\\b\\d+\\.\\d*\\b/, \"number.float\"],\n//       [/\\b0x[0-9a-fA-F]+\\b/, \"number.hex\"],\n//       [/\\b\\d+\\b/, \"number\"],\n//     ],\n\n//     booleans: [[/\\b(true|false|yes|no|on|off)\\b/, \"constant.language.boolean\"]],\n\n//     strings: [\n//       [/\"([^\"\\\\]|\\\\.)*$/, \"string.invalid\"], // non-terminated string\n//       [/'([^'\\\\]|\\\\.)*$/, \"string.invalid\"], // non-terminated string\n//       [/\"/, \"string\", \"@string_double\"],\n//       [/'/, \"string\", \"@string_single\"],\n//     ],\n\n//     string_double: [\n//       [/[^\\\\\"]+/, \"string\"],\n//       [/@escapes/, \"string.escape\"],\n//       [/\\\\./, \"string.escape.invalid\"],\n//       [/\"/, \"string\", \"@pop\"],\n//     ],\n\n//     string_single: [\n//       [/[^\\\\']+/, \"string\"],\n//       [/@escapes/, \"string.escape\"],\n//       [/\\\\./, \"string.escape.invalid\"],\n//       [/'/, \"string\", \"@pop\"],\n//     ],\n\n//     constants: [[/\\b(null|~)\\b/, \"constant.language.null\"]],\n//   },\n// };\n\n// monaco.languages.register({ id: \"yaml\" });\n// monaco.languages.setMonarchTokensProvider(\"yaml\", yaml_language);\n// monaco.languages.setLanguageConfiguration(\"yaml\", yaml_conf);\n"
  },
  {
    "path": "frontend/src/pages/alerts.tsx",
    "content": "import { AlertsTable } from \"@components/alert/table\";\nimport { Page } from \"@components/layouts\";\nimport { useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Button } from \"@ui/button\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { Switch } from \"@ui/switch\";\nimport {\n  AlertTriangle,\n  Box,\n  ChevronLeft,\n  ChevronRight,\n  MinusCircle,\n} from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { UsableResource } from \"@types\";\nimport { SelectSeparator } from \"@radix-ui/react-select\";\nimport { ResourceComponents } from \"@components/resources\";\nimport { ResourceSelector } from \"@components/resources/common\";\n\nconst ALERT_TYPES_BY_RESOURCE: { [key: string]: Types.AlertData[\"type\"][] } = {\n  Server: [\"ServerUnreachable\", \"ServerCpu\", \"ServerMem\", \"ServerDisk\"],\n  Stack: [\"StackStateChange\", \"StackImageUpdateAvailable\", \"StackAutoUpdated\"],\n  Deployment: [\n    \"ContainerStateChange\",\n    \"DeploymentImageUpdateAvailable\",\n    \"DeploymentAutoUpdated\",\n  ],\n  Build: [\"BuildFailed\"],\n  Repo: [\"RepoBuildFailed\"],\n  ResourceSync: [\"ResourceSyncPendingUpdates\"],\n};\n\nconst FALLBACK_ALERT_TYPES = [\n  ...Object.values(ALERT_TYPES_BY_RESOURCE).flat(),\n  \"AwsBuilderTerminationFailed\",\n];\n\nexport default function AlertsPage() {\n  const [page, setPage] = useState(0);\n  const [params, setParams] = useSearchParams();\n\n  const { type, id, alert_type, open } = useMemo(\n    () => ({\n      type: (params.get(\"type\") as UsableResource) ?? undefined,\n      id: params.get(\"id\") ?? undefined,\n      alert_type: (params.get(\"alert\") as Types.AlertData[\"type\"]) ?? undefined,\n      open: params.get(\"open\") === \"true\" || undefined,\n    }),\n    [params]\n  );\n\n  const { data: alerts } = useRead(\"ListAlerts\", {\n    query: {\n      \"target.type\": type,\n      \"target.id\": id,\n      \"data.type\": alert_type,\n      resolved: !open,\n    },\n    page,\n  });\n\n  const alert_types: string[] = type\n    ? (ALERT_TYPES_BY_RESOURCE[type] ?? FALLBACK_ALERT_TYPES)\n    : FALLBACK_ALERT_TYPES;\n\n  return (\n    <Page\n      title=\"Alerts\"\n      icon={<AlertTriangle className=\"w-8\" />}\n      actions={\n        <>\n          <div className=\"flex items-center md:justify-end gap-4 flex-wrap\">\n            {/* resource type */}\n            <Select\n              value={type ?? \"All\"}\n              onValueChange={(type) => {\n                const p = new URLSearchParams(params.toString());\n                type === \"all\" ? p.delete(\"type\") : p.set(\"type\", type);\n                p.delete(\"id\");\n                p.delete(\"operation\");\n                setParams(p);\n              }}\n            >\n              <SelectTrigger className=\"w-48\">\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"All\">\n                  <div className=\"flex items-center gap-2\">\n                    <Box className=\"w-4 text-muted-foreground\" />\n                    All Resources\n                  </div>\n                </SelectItem>\n                <SelectSeparator />\n                {Object.keys(ALERT_TYPES_BY_RESOURCE).map((type) => {\n                  const Icon = ResourceComponents[type].Icon;\n                  return (\n                    <SelectItem key={type} value={type}>\n                      <div className=\"flex items-center gap-2\">\n                        <span className=\"text-muted-foreground\">\n                          <Icon />\n                        </span>\n                        {type}\n                      </div>\n                    </SelectItem>\n                  );\n                })}\n              </SelectContent>\n            </Select>\n\n            {/* resource id */}\n            {type && (\n              <ResourceSelector\n                type={type}\n                selected={id}\n                onSelect={(id) => {\n                  const p = new URLSearchParams(params.toString());\n                  id === \"all\" ? p.delete(\"id\") : p.set(\"id\", id);\n                  setParams(p);\n                }}\n              />\n            )}\n\n            {/* operation */}\n            <Select\n              value={alert_type ?? \"All\"}\n              onValueChange={(alert) => {\n                const p = new URLSearchParams(params.toString());\n                alert === \"All\" ? p.delete(\"alert\") : p.set(\"alert\", alert);\n                setParams(p);\n              }}\n            >\n              <SelectTrigger className=\"w-64 overflow-ellipsis\">\n                <SelectValue placeholder=\"Alert Type\" />\n              </SelectTrigger>\n              <SelectContent align=\"end\">\n                <SelectItem value=\"All\">\n                  <div className=\"flex items-center gap-2\">All Alerts</div>\n                </SelectItem>\n                {alert_types.map((variant) => (\n                  <SelectItem key={variant} value={variant}>\n                    {variant}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n\n            {/* only open */}\n            <div\n              className=\"px-4 h-9 flex items-center gap-4 border rounded-md\"\n              onClick={() => {\n                const p = new URLSearchParams(params.toString());\n                open ? p.delete(\"open\") : p.set(\"open\", \"true\");\n                setParams(p);\n              }}\n            >\n              <p className=\"text-sm text-muted-foreground\">Only Open</p>\n              <Switch checked={open} className=\"pointer-events-none\" />\n            </div>\n\n            {/* reset */}\n            <Button\n              size=\"icon\"\n              onClick={() => setParams({})}\n              variant=\"secondary\"\n            >\n              <MinusCircle className=\"w-4\" />\n            </Button>\n          </div>\n        </>\n      }\n    >\n      <div className=\"flex flex-col gap-2\">\n        <AlertsTable alerts={alerts?.alerts ?? []} showResolved />\n        <div className=\"flex gap-4 items-center\">\n          <Button\n            variant=\"outline\"\n            onClick={() => setPage(page - 1)}\n            disabled={page === 0}\n            size=\"icon\"\n          >\n            <ChevronLeft className=\"w-4\" />\n          </Button>\n          {Array.from(new Array(page + 1)).map((_, i) => (\n            <Button\n              key={i}\n              onClick={() => setPage(i)}\n              variant={page === i ? \"secondary\" : \"outline\"}\n            >\n              {i + 1}\n            </Button>\n          ))}\n          {/* Page: {page + 1} */}\n          <Button\n            variant=\"outline\"\n            onClick={() => alerts?.next_page && setPage(alerts.next_page)}\n            disabled={!alerts?.next_page}\n            size=\"icon\"\n          >\n            <ChevronRight className=\"w-4\" />\n          </Button>\n        </div>\n      </div>\n    </Page>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/containers.tsx",
    "content": "import { Page } from \"@components/layouts\";\nimport { ResourceLink } from \"@components/resources/common\";\nimport {\n  ContainerPortsTableView,\n  DockerResourceLink,\n  StatusBadge,\n} from \"@components/util\";\nimport { container_state_intention } from \"@lib/color\";\nimport { useDebounce, useRead } from \"@lib/hooks\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { Input } from \"@ui/input\";\nimport { MultiSelect } from \"@ui/multi-select\";\nimport { Box, Search, RotateCcw } from \"lucide-react\";\nimport { Button } from \"@ui/button\";\nimport { Fragment, useCallback, useMemo, useState } from \"react\";\n\nexport default function ContainersPage() {\n  const [search, setSearch] = useState(\"\");\n  const [selectedServers, setSelectedServers] = useState<string[]>([]);\n\n  const debouncedSearch = useDebounce(search, 300);\n\n  const searchSplit = debouncedSearch\n    .toLowerCase()\n    .split(\" \")\n    .filter((term) => term);\n\n  const servers = useRead(\"ListServers\", {}).data;\n  const serverOptions = useMemo(\n    () =>\n      servers?.map((server) => ({\n        label: server.name,\n        value: server.id,\n      })) || [],\n    [servers]\n  );\n\n  const serverName = useCallback(\n    (id: string) => servers?.find((server) => server.id === id)?.name,\n    [servers]\n  );\n\n  const _containers = useRead(\"ListAllDockerContainers\", {}).data;\n\n  const containers = useMemo(\n    () =>\n      _containers?.filter((c) => {\n        if (searchSplit.length > 0) {\n          const lower = c.name.toLowerCase();\n          const searchMatch = searchSplit.every((search) =>\n            lower.includes(search)\n          );\n          if (!searchMatch) return false;\n        }\n\n        if (selectedServers.length > 0) {\n          return selectedServers.includes(c.server_id!);\n        }\n\n        return true;\n      }),\n    [_containers, searchSplit, selectedServers]\n  );\n\n  const clearAllServers = useCallback(() => {\n    setSelectedServers([]);\n  }, []);\n\n  return (\n    <Page\n      title=\"Containers\"\n      subtitle={\n        <div className=\"text-muted-foreground\">\n          See all containers across all servers\n        </div>\n      }\n      icon={<Box className=\"w-8 h-8\" />}\n    >\n      <div className=\"flex flex-col gap-4\">\n        <div className=\"flex items-center justify-between gap-4\">\n          <div className=\"flex items-center gap-2 flex-wrap\">\n            {/* Server Filter Multi-Select */}\n            <div className=\"w-[280px]\">\n              <MultiSelect\n                options={serverOptions}\n                value={selectedServers}\n                onChange={setSelectedServers}\n                placeholder=\"Filter by server...\"\n                className=\"w-full h-10\"\n              />\n            </div>\n\n            {/* Reset Server Filter Button */}\n            {selectedServers.length > 0 && (\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={clearAllServers}\n                className=\"h-10 px-3\"\n              >\n                <RotateCcw className=\"w-4 h-4 mr-1\" />\n                Reset\n              </Button>\n            )}\n          </div>\n\n          {/* Search Input */}\n          <div className=\"relative\">\n            <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n            <Input\n              value={search}\n              onChange={(e) => setSearch(e.target.value)}\n              placeholder=\"search...\"\n              className=\"pl-8 w-[200px] lg:w-[300px] py-0 h-10\"\n            />\n          </div>\n        </div>\n\n        <DataTable\n          data={containers ?? []}\n          tableKey=\"containers-page-v1\"\n          columns={[\n            {\n              accessorKey: \"name\",\n              size: 260,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Name\" />\n              ),\n              cell: ({ row }) => (\n                <DockerResourceLink\n                  type=\"container\"\n                  server_id={row.original.server_id!}\n                  name={row.original.name}\n                />\n              ),\n            },\n            {\n              accessorKey: \"server_id\",\n              size: 200,\n              sortingFn: (a, b) => {\n                const sa = serverName(a.original.server_id!);\n                const sb = serverName(b.original.server_id!);\n\n                if (!sa && !sb) return 0;\n                if (!sa) return -1;\n                if (!sb) return 1;\n\n                if (sa > sb) return 1;\n                else if (sa < sb) return -1;\n                else return 0;\n              },\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Server\" />\n              ),\n              cell: ({ row }) => (\n                <ResourceLink type=\"Server\" id={row.original.server_id!} />\n              ),\n            },\n            {\n              accessorKey: \"image\",\n              size: 300,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Image\" />\n              ),\n              cell: ({ row }) => (\n                <DockerResourceLink\n                  type=\"image\"\n                  server_id={row.original.server_id!}\n                  name={row.original.image}\n                  id={row.original.image_id}\n                />\n              ),\n            },\n            {\n              accessorKey: \"state\",\n              size: 160,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"State\" />\n              ),\n              cell: ({ row }) => {\n                const state = row.original?.state;\n                return (\n                  <StatusBadge\n                    text={state}\n                    intent={container_state_intention(state)}\n                  />\n                );\n              },\n            },\n            {\n              accessorKey: \"networks.0\",\n              size: 200,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Networks\" />\n              ),\n              cell: ({ row }) =>\n                (row.original.networks?.length ?? 0) > 0 ? (\n                  <div className=\"flex items-center gap-x-2 flex-wrap\">\n                    {row.original.networks?.map((network, i) => (\n                      <Fragment key={network}>\n                        <DockerResourceLink\n                          type=\"network\"\n                          server_id={row.original.server_id!}\n                          name={network}\n                        />\n                        {i !== row.original.networks!.length - 1 && (\n                          <div className=\"text-muted-foreground\">|</div>\n                        )}\n                      </Fragment>\n                    ))}\n                  </div>\n                ) : (\n                  row.original.network_mode && (\n                    <DockerResourceLink\n                      type=\"network\"\n                      server_id={row.original.server_id!}\n                      name={row.original.network_mode}\n                    />\n                  )\n                ),\n            },\n            {\n              accessorKey: \"ports.0\",\n              size: 200,\n              sortingFn: (a, b) => {\n                const getMinHostPort = (row: typeof a) => {\n                  const ports = row.original.ports ?? [];\n                  if (!ports.length) return Number.POSITIVE_INFINITY;\n                  const nums = ports\n                    .map((p) => p.PublicPort)\n                    .filter((p): p is number => typeof p === \"number\")\n                    .map((n) => Number(n));\n                  if (!nums.length || nums.some((n) => Number.isNaN(n))) {\n                    return Number.POSITIVE_INFINITY;\n                  }\n                  return Math.min(...nums);\n                };\n                const pa = getMinHostPort(a);\n                const pb = getMinHostPort(b);\n                return pa === pb ? 0 : pa > pb ? 1 : -1;\n              },\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Ports\" />\n              ),\n              cell: ({ row }) => (\n                <ContainerPortsTableView\n                  ports={row.original.ports ?? []}\n                  server_id={row.original.server_id}\n                />\n              ),\n            },\n          ]}\n        />\n      </div>\n    </Page>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/home/all_resources.tsx",
    "content": "import { ExportButton } from \"@components/export\";\nimport { Page, Section } from \"@components/layouts\";\nimport { ResourceComponents } from \"@components/resources\";\nimport { TagsFilter } from \"@components/tags\";\nimport { ShowHideButton } from \"@components/util\";\nimport {\n  useFilterResources,\n  useNoResources,\n  useRead,\n  useTagsFilter,\n  useUser,\n} from \"@lib/hooks\";\nimport { cn } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { RequiredResourceComponents, UsableResource } from \"@types\";\nimport { Input } from \"@ui/input\";\nimport { AlertTriangle } from \"lucide-react\";\nimport { useState } from \"react\";\n\nexport default function AllResources() {\n  const [search, setSearch] = useState(\"\");\n  const tags = useTagsFilter();\n  const noResources = useNoResources();\n  const user = useUser().data!;\n  return (\n    <Page\n      titleOther={\n        <div className=\"flex items-center justify-between\">\n          <Input\n            value={search}\n            onChange={(e) => setSearch(e.target.value)}\n            placeholder=\"search...\"\n            className=\"w-[200px] lg:w-[300px]\"\n          />\n\n          <div className=\"flex items-center gap-2\">\n            <TagsFilter />\n            <ExportButton tags={tags} />\n          </div>\n        </div>\n      }\n    >\n      {noResources && (\n        <div className=\"flex items-center gap-4 px-2 text-muted-foreground\">\n          <AlertTriangle className=\"w-4 h-4\" />\n          <p className=\"text-lg\">\n            No resources found.{\" \"}\n            {user.admin\n              ? \"To get started, create a server.\"\n              : \"Contact an admin for access to resources.\"}\n          </p>\n        </div>\n      )}\n      <div className=\"flex flex-col gap-6\">\n        {Object.entries(ResourceComponents).map(([type, Components]) => (\n          <TableSection\n            key={type}\n            type={type}\n            Components={Components}\n            search={search}\n          />\n        ))}\n      </div>\n    </Page>\n  );\n}\n\nconst TableSection = ({\n  type,\n  Components,\n  search,\n}: {\n  type: string;\n  Components: RequiredResourceComponents;\n  search?: string;\n}) => {\n  const resources = useRead(`List${type as UsableResource}s`, {}).data;\n\n  const filtered = useFilterResources(\n    resources as Types.ResourceListItem<unknown>[],\n    search\n  );\n\n  let count = filtered.length;\n\n  const [show, setShow] = useState(true);\n\n  if (!count) return;\n\n  return (\n    <Section\n      key={type}\n      title={type + \"s\"}\n      icon={<Components.Icon />}\n      actions={<ShowHideButton show={show} setShow={setShow} />}\n    >\n      <div className={cn(\"border-b\", show && \"pb-8\")}>\n        {show && <Components.Table resources={filtered ?? []} />}\n      </div>\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/home/dashboard.tsx",
    "content": "import { ExportButton } from \"@components/export\";\nimport { Page, Section } from \"@components/layouts\";\nimport { ResourceComponents } from \"@components/resources\";\nimport { ResourceLink, ResourceNameSimple } from \"@components/resources/common\";\nimport { ServerStatsMini } from \"@components/resources/server\";\nimport { TagsWithBadge } from \"@components/tags\";\nimport { StatusBadge, TemplateMarker } from \"@components/util\";\nimport { useDashboardPreferences } from \"@lib/dashboard-preferences\";\nimport { Button } from \"@ui/button\";\nimport { Eye, EyeOff } from \"lucide-react\";\nimport {\n  action_state_intention,\n  build_state_intention,\n  ColorIntention,\n  hex_color_by_intention,\n  procedure_state_intention,\n  repo_state_intention,\n  text_color_class_by_intention,\n} from \"@lib/color\";\nimport { useNoResources, useRead, useUser } from \"@lib/hooks\";\nimport { cn, usableResourcePath } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { UsableResource } from \"@types\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { AlertTriangle, Box, Circle, History } from \"lucide-react\";\nimport { PieChart } from \"react-minimal-pie-chart\";\nimport { Link } from \"react-router-dom\";\nimport { UpdateAvailable as StackUpdateAvailable } from \"@components/resources/stack\";\nimport { UpdateAvailable as DeploymentUpdateAvailable } from \"@components/resources/deployment\";\n\nexport default function Dashboard() {\n  const noResources = useNoResources();\n  const user = useUser().data!;\n  const { preferences, updatePreference } = useDashboardPreferences();\n\n  return (\n    <>\n      <ActiveResources />\n      <Page\n        title=\"Dashboard\"\n        icon={<Box className=\"w-8 h-8\" />}\n        actions={\n          <div className=\"flex items-center gap-2\">\n            <Button\n              variant=\"outline\"\n              onClick={() =>\n                updatePreference(\n                  \"showServerStats\",\n                  !preferences.showServerStats,\n                )\n              }\n              className=\"flex items-center gap-2\"\n            >\n              {preferences.showServerStats ? (\n                <>\n                  <EyeOff className=\"w-4 h-4\" />\n                  <span>Hide Server Stats</span>\n                </>\n              ) : (\n                <>\n                  <Eye className=\"w-4 h-4\" />\n                  <span>Show Server Stats</span>\n                </>\n              )}\n            </Button>\n            <ExportButton />\n          </div>\n        }\n      >\n        <div className=\"flex flex-col gap-6 w-full\">\n          {noResources && (\n            <div className=\"flex items-center gap-4 px-2 text-muted-foreground\">\n              <AlertTriangle className=\"w-4 h-4\" />\n              <p className=\"text-lg\">\n                No resources found.{\" \"}\n                {user.admin\n                  ? \"To get started, create a server.\"\n                  : \"Contact an admin for access to resources.\"}\n              </p>\n            </div>\n          )}\n          <ResourceRow type=\"Server\" />\n          <ResourceRow type=\"Stack\" />\n          <ResourceRow type=\"Deployment\" />\n          <ResourceRow type=\"Build\" />\n          <ResourceRow type=\"Repo\" />\n          <ResourceRow type=\"Procedure\" />\n          <ResourceRow type=\"Action\" />\n          <ResourceRow type=\"ResourceSync\" />\n        </div>\n      </Page>\n    </>\n  );\n}\n\nconst ResourceRow = ({ type }: { type: UsableResource }) => {\n  const _recents = useUser().data?.recents?.[type]?.slice(0, 6);\n  const _resources = useRead(`List${type}s`, {}).data;\n  const recents = _recents?.filter(\n    (recent) => !_resources?.every((resource) => resource.id !== recent),\n  );\n  const resources = _resources\n    ?.filter((r) => !recents?.includes(r.id))\n    .map((r) => r.id);\n  const ids = [\n    ...(recents ?? []),\n    ...(resources?.slice(0, 6 - (recents?.length || 0)) ?? []),\n  ];\n  if (ids.length === 0) return;\n  const Components = ResourceComponents[type];\n  const name = type === \"ResourceSync\" ? \"Resource Sync\" : type;\n  return (\n    <div className=\"border rounded-md flex flex-col md:flex-row\">\n      <Link\n        to={`/${usableResourcePath(type)}`}\n        className=\"shrink-0 px-6 py-4 flex flex-col justify-center lg:border-r group bg-accent/50 hover:bg-accent/15 transition-colors\"\n      >\n        <div className=\"flex items-center gap-4 text-xl group-hover:underline\">\n          <Components.Icon />\n          {name}s\n        </div>\n        <Components.Dashboard />\n      </Link>\n      <div className=\"px-6 py-4 w-full flex flex-col gap-4\">\n        <p className=\"text-sm text-muted-foreground flex items-center gap-2\">\n          <History className=\"w-3\" />\n          Recently Viewed\n        </p>\n        <div className=\"grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3 gap-4 auto-rows-max\">\n          {ids.map((id, i) => (\n            <RecentCard\n              key={type + id}\n              type={type}\n              id={id}\n              className={\n                i > 3\n                  ? \"hidden 2xl:flex\"\n                  : i > 1\n                    ? \"hidden sm:flex md:hidden xl:flex\"\n                    : undefined\n              }\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst RecentCard = ({\n  type,\n  id,\n  className,\n}: {\n  type: UsableResource;\n  id: string;\n  className?: string;\n}) => {\n  const Components = ResourceComponents[type];\n  const resource = Components.list_item(id);\n  const { preferences } = useDashboardPreferences();\n\n  if (!resource) return null;\n\n  const tags = resource?.tags;\n  const showServerStats = type === \"Server\" && preferences.showServerStats;\n\n  return (\n    <Link\n      to={`${usableResourcePath(type)}/${id}`}\n      className={cn(\n        \"w-full px-3 py-2 border rounded-md hover:bg-accent/25 hover:-translate-y-1 transition-all duration-1000 linear flex flex-col justify-between\",\n        showServerStats ? \"min-h-32\" : \"h-20\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex flex-wrap items-center gap-2 text-sm text-nowrap\">\n          <Components.Icon id={id} />\n          <ResourceNameSimple type={type} id={id} />\n          {resource.template && <TemplateMarker type={type} />}\n        </div>\n        {type === \"Deployment\" && <DeploymentUpdateAvailable id={id} small />}\n        {type === \"Stack\" && <StackUpdateAvailable id={id} small />}\n      </div>\n\n      <div \n        className={cn(\n          \"overflow-hidden w-full transition-opacity transition-all duration-1000 linear\",\n          showServerStats \n            ? \"max-h-40 opacity-100 py-2\" \n            : \"max-h-0 opacity-0 py-0\"\n        )}\n      >\n        <div className=\"flex flex-col gap-2\">\n          <ServerStatsMini id={id} enabled={showServerStats} />\n        </div>\n      </div>\n\n      <div className=\"flex flex-row gap-2 w-full py-1\">\n        <TagsWithBadge tag_ids={tags} />\n      </div>\n    </Link>\n  );\n};\n\nexport type DashboardPieChartItem = {\n  title: string;\n  intention: ColorIntention;\n  value: number;\n};\n\nexport const DashboardPieChart = ({\n  data: _data,\n}: {\n  data: Array<DashboardPieChartItem | false | undefined>;\n}) => {\n  const data = _data.filter((d) => d) as Array<DashboardPieChartItem>;\n  return (\n    <div className=\"flex items-center gap-8\">\n      <div className=\"flex flex-col gap-2 w-28\">\n        {data.map(({ title, value, intention }) => (\n          <p key={title} className=\"flex gap-2 text-xs text-muted-foreground\">\n            <span\n              className={cn(\n                \"font-bold\",\n                text_color_class_by_intention(intention)\n              )}\n            >\n              {value}\n            </span>\n            {title}\n          </p>\n        ))}\n      </div>\n      <PieChart\n        className=\"w-32 h-32\"\n        radius={42}\n        lineWidth={30}\n        data={data.map(({ title, value, intention }) => ({\n          title,\n          value,\n          color: hex_color_by_intention(intention),\n        }))}\n      />\n    </div>\n  );\n};\n\nconst ActiveResources = () => {\n  const builds =\n    useRead(\"ListBuilds\", {}).data?.filter(\n      (build) => build.info.state === Types.BuildState.Building\n    ) ?? [];\n  const repos =\n    useRead(\"ListRepos\", {}).data?.filter((repo) =>\n      [\n        Types.RepoState.Building,\n        Types.RepoState.Cloning,\n        Types.RepoState.Pulling,\n      ].includes(repo.info.state)\n    ) ?? [];\n  const procedures =\n    useRead(\"ListProcedures\", {}).data?.filter(\n      (procedure) => procedure.info.state === Types.ProcedureState.Running\n    ) ?? [];\n  const actions =\n    useRead(\"ListActions\", {}).data?.filter(\n      (action) => action.info.state === Types.ActionState.Running\n    ) ?? [];\n\n  const resources = [\n    ...(builds ?? []).map((build) => ({\n      type: \"Build\" as UsableResource,\n      id: build.id,\n      state: (\n        <StatusBadge\n          text={build.info.state}\n          intent={build_state_intention(build.info.state)}\n        />\n      ),\n    })),\n    ...(repos ?? []).map((repo) => ({\n      type: \"Repo\" as UsableResource,\n      id: repo.id,\n      state: (\n        <StatusBadge\n          text={repo.info.state}\n          intent={repo_state_intention(repo.info.state)}\n        />\n      ),\n    })),\n    ...(procedures ?? []).map((procedure) => ({\n      type: \"Procedure\" as UsableResource,\n      id: procedure.id,\n      state: (\n        <StatusBadge\n          text={procedure.info.state}\n          intent={procedure_state_intention(procedure.info.state)}\n        />\n      ),\n    })),\n    ...(actions ?? []).map((action) => ({\n      type: \"Action\" as UsableResource,\n      id: action.id,\n      state: (\n        <StatusBadge\n          text={action.info.state}\n          intent={action_state_intention(action.info.state)}\n        />\n      ),\n    })),\n  ];\n\n  if (resources.length === 0) return null;\n\n  return (\n    <div className=\"mb-12\">\n      <Section\n        title=\"Active\"\n        icon={\n          <Circle className=\"w-4 h-4 stroke-none transition-colors fill-green-500\" />\n        }\n      >\n        <DataTable\n          tableKey=\"active-resources\"\n          data={resources}\n          columns={[\n            {\n              accessorKey: \"name\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Name\" />\n              ),\n              cell: ({ row }) => (\n                <ResourceLink type={row.original.type} id={row.original.id} />\n              ),\n            },\n            {\n              accessorKey: \"type\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Resource\" />\n              ),\n            },\n            {\n              header: \"State\",\n              cell: ({ row }) => row.original.state,\n            },\n          ]}\n        />\n      </Section>\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/home/index.tsx",
    "content": "import { homeViewAtom } from \"@main\";\nimport { useAtom } from \"jotai\";\nimport { useSetTitle } from \"@lib/hooks\";\nimport { lazy } from \"react\";\n\nconst Dashboard = lazy(() => import(\"./dashboard\"));\nconst AllResources = lazy(() => import(\"./all_resources\"));\nconst Tree = lazy(() => import(\"./tree\"));\n\nexport default function Home() {\n  useSetTitle();\n  const [view] = useAtom(homeViewAtom);\n  switch (view) {\n    case \"Dashboard\":\n      return <Dashboard />;\n    case \"Resources\":\n      return <AllResources />;\n    case \"Tree\":\n      return <Tree />;\n  }\n}\n"
  },
  {
    "path": "frontend/src/pages/home/tree.tsx",
    "content": "import { ExportButton } from \"@components/export\";\nimport { Page, Section } from \"@components/layouts\";\nimport { ResourceComponents } from \"@components/resources\";\nimport { DeploymentTable } from \"@components/resources/deployment/table\";\nimport { ServerComponents } from \"@components/resources/server\";\nimport { TagsFilter, TagsWithBadge } from \"@components/tags\";\nimport { useFilterResources, useRead, useTagsFilter } from \"@lib/hooks\";\nimport { Button } from \"@ui/button\";\nimport { Card, CardHeader, CardTitle } from \"@ui/card\";\nimport { Input } from \"@ui/input\";\nimport { atom, useAtom } from \"jotai\";\nimport { Fragment, useState } from \"react\";\nimport { Link } from \"react-router-dom\";\n\nconst searchAtom = atom(\"\");\n\nexport default function Tree() {\n  const [search, setSearch] = useAtom(searchAtom);\n  const tags = useTagsFilter();\n  const servers = useRead(\"ListServers\", { query: { tags } }).data;\n  return (\n    <Page\n      titleOther={\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex gap-4 items-center\">\n            <Input\n              value={search}\n              onChange={(e) => setSearch(e.target.value)}\n              placeholder=\"search...\"\n              className=\"w-[200px] lg:w-[300px]\"\n            />\n            <ExportButton tags={tags} />\n          </div>\n          <TagsFilter />\n        </div>\n      }\n    >\n      <Section>\n        <div className=\"grid gap-6\">\n          {servers?.map((server) => <Server key={server.id} id={server.id} />)}\n        </div>\n      </Section>\n    </Page>\n  );\n}\n\nconst Server = ({ id }: { id: string }) => {\n  const [search] = useAtom(searchAtom);\n  // const [open, setOpen] = useLocalStorage(`server-tree-open-${id}`, false);\n  const [open, setOpen] = useState(false);\n  const server = useRead(\"ListServers\", {}).data?.find(\n    (server) => server.id === id\n  );\n  const deployments = useRead(\"ListDeployments\", {}).data?.filter(\n    (deployment) => deployment.info.server_id === id\n  );\n  const filtered = useFilterResources(deployments, search);\n  return (\n    <div className=\"grid gap-2\">\n      <Card\n        className=\"h-full hover:bg-accent/50 group-focus:bg-accent/50 transition-colors cursor-pointer\"\n        onClick={() => setOpen(!open)}\n      >\n        <CardHeader className=\"p-4 flex-row justify-between items-center\">\n          <CardTitle>{server?.name}</CardTitle>\n          <div className=\"flex gap-3 justify-between items-center\">\n            <TagsWithBadge tag_ids={server?.tags} />\n            {server?.id && (\n              <div className=\"flex gap-4 items-center\">\n                {Object.entries(ServerComponents.Info).map(([key, Info], i) => (\n                  <Fragment key={key}>\n                    {i !== 0 && \"|\"} <Info id={server.id} />\n                  </Fragment>\n                ))}\n              </div>\n            )}\n            <Link to={`/servers/${server?.id}`}>\n              <Button variant=\"outline\">\n                <ResourceComponents.Server.Icon id={server?.id} />\n              </Button>\n            </Link>\n          </div>\n        </CardHeader>\n      </Card>\n      {open && <DeploymentTable deployments={filtered ?? []} />}\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/login.tsx",
    "content": "import { Button } from \"@ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@ui/card\";\nimport { Input } from \"@ui/input\";\nimport { Label } from \"@ui/label\";\nimport {\n  LOGIN_TOKENS,\n  useAuth,\n  useLoginOptions,\n  useUserInvalidate,\n} from \"@lib/hooks\";\nimport { useRef } from \"react\";\nimport { ThemeToggle } from \"@ui/theme\";\nimport { KOMODO_BASE_URL } from \"@main\";\nimport { KeyRound, X } from \"lucide-react\";\nimport { cn } from \"@lib/utils\";\nimport { useToast } from \"@ui/use-toast\";\nimport { Types } from \"komodo_client\";\n\ntype OauthProvider = \"Github\" | \"Google\" | \"OIDC\";\n\nconst login_with_oauth = (provider: OauthProvider) => {\n  const _redirect = location.pathname.startsWith(\"/login\")\n    ? location.origin +\n      (new URLSearchParams(location.search).get(\"backto\") ?? \"\")\n    : location.href;\n  const redirect = encodeURIComponent(_redirect);\n  location.replace(\n    `${KOMODO_BASE_URL}/auth/${provider.toLowerCase()}/login?redirect=${redirect}`\n  );\n};\n\nexport default function Login() {\n  const options = useLoginOptions().data;\n  const userInvalidate = useUserInvalidate();\n  const { toast } = useToast();\n  const formRef = useRef<HTMLFormElement>(null);\n\n  // If signing in another user, need to redirect away from /login manually\n  const maybeNavigate = location.pathname.startsWith(\"/login\")\n    ? () =>\n        location.replace(\n          new URLSearchParams(location.search).get(\"backto\") ?? \"/\"\n        )\n    : undefined;\n\n  const onSuccess = ({ user_id, jwt }: Types.JwtResponse) => {\n    LOGIN_TOKENS.add_and_change(user_id, jwt);\n    userInvalidate();\n    maybeNavigate?.();\n  };\n\n  const { mutate: signup, isPending: signupPending } = useAuth(\n    \"SignUpLocalUser\",\n    {\n      onSuccess,\n      onError: (e: any) => {\n        const message = e?.response?.data?.error as string | undefined;\n        if (message) {\n          toast({\n            title: `Failed to sign up user. '${message}'`,\n            variant: \"destructive\",\n          });\n          console.error(e);\n        } else {\n          toast({\n            title: \"Failed to sign up user. See console log for details.\",\n            variant: \"destructive\",\n          });\n          console.error(e);\n        }\n      },\n    }\n  );\n  const { mutate: login, isPending: loginPending } = useAuth(\"LoginLocalUser\", {\n    onSuccess,\n    onError: (e: any) => {\n      const message = e?.response?.data?.error as string | undefined;\n      if (message) {\n        toast({\n          title: `Failed to login user. '${message}'`,\n          variant: \"destructive\",\n        });\n        console.error(e);\n      } else {\n        toast({\n          title: \"Failed to login user. See console log for details.\",\n          variant: \"destructive\",\n        });\n        console.error(e);\n      }\n    },\n  });\n\n  const getFormCredentials = () => {\n    if (!formRef.current) return undefined;\n    const fd = new FormData(formRef.current);\n    const username = String(fd.get(\"username\") ?? \"\");\n    const password = String(fd.get(\"password\") ?? \"\");\n    return { username, password };\n  };\n\n  const handleLogin = () => {\n    const creds = getFormCredentials();\n    if (!creds) return;\n    login(creds);\n  };\n  \n  const handleSubmit = (e: any) => {\n    e.preventDefault();\n    handleLogin();\n  };\n  \n  const handleSignUp = () => {\n    const creds = getFormCredentials();\n    if (!creds) return;\n    signup(creds);\n  };\n\n  const no_auth_configured =\n    options !== undefined &&\n    Object.values(options).every((value) => value === false);\n\n  const show_sign_up = options !== undefined && !options.registration_disabled;\n\n  // Otherwise just standard login\n  return (\n    <div className=\"flex flex-col min-h-screen\">\n      <div className=\"container flex justify-end items-center h-16\">\n        <ThemeToggle />\n      </div>\n      <div\n        className={cn(\n          \"flex justify-center items-center container\",\n          options?.local ? \"mt-32\" : \"mt-64\"\n        )}\n      >\n        <Card className=\"w-full max-w-[500px] place-self-center\">\n          <CardHeader className=\"flex-row justify-between\">\n            <div className=\"flex gap-4 items-center\">\n              <img src=\"/komodo-512x512.png\" className=\"w-[32px] h-[32px]\" />\n              <div>\n                <CardTitle className=\"text-xl\">Komodo</CardTitle>{\" \"}\n                <CardDescription>Log In</CardDescription>\n              </div>\n            </div>\n            <div className=\"flex gap-2\">\n              {(\n                [\n                  [options?.google, \"Google\"],\n                  [options?.github, \"Github\"],\n                  [options?.oidc, \"OIDC\"],\n                ] as Array<[boolean | undefined, OauthProvider]>\n              ).map(\n                ([enabled, provider]) =>\n                  enabled && (\n                    <Button\n                      key={provider}\n                      variant=\"outline\"\n                      className=\"flex gap-2 px-3 items-center\"\n                      onClick={() => login_with_oauth(provider)}\n                    >\n                      {provider}\n                      {provider === \"OIDC\" ? (\n                        <KeyRound className=\"w-4 h-4\" />\n                      ) : (\n                        <img\n                          src={`/icons/${provider.toLowerCase()}.svg`}\n                          alt={provider}\n                          className=\"w-4 h-4\"\n                        />\n                      )}\n                    </Button>\n                  )\n              )}\n              {no_auth_configured && (\n                <Button variant=\"destructive\" size=\"icon\">\n                  {\" \"}\n                  <X className=\"w-4 h-4\" />{\" \"}\n                </Button>\n              )}\n            </div>\n          </CardHeader>\n          {options?.local && (\n            <form\n              ref={formRef}\n              onSubmit={handleSubmit}\n              autoComplete=\"on\"\n            >\n              <CardContent className=\"flex flex-col justify-center w-full gap-4\">\n                <div className=\"flex flex-col gap-2\">\n                  <Label htmlFor=\"username\">Username</Label>\n                  <Input\n                    id=\"username\"\n                    name=\"username\"\n                    autoComplete=\"username\"\n                    autoCapitalize=\"none\"\n                    autoCorrect=\"off\"\n                    autoFocus\n                  />\n                </div>\n                <div className=\"flex flex-col gap-2\">\n                  <Label htmlFor=\"password\">Password</Label>\n                  <Input\n                    id=\"password\"\n                    name=\"password\"\n                    type=\"password\"\n                    autoComplete=\"current-password\"\n                  />\n                </div>\n              </CardContent>\n              <CardFooter className=\"flex gap-4 w-full justify-end\">\n                {show_sign_up && (\n                  <Button\n                    variant=\"outline\"\n                    type=\"button\"\n                    value=\"signup\"\n                    onClick={handleSignUp}\n                    disabled={signupPending}\n                  >\n                    Sign Up\n                  </Button>\n                )}\n                <Button\n                  variant=\"default\"\n                  type=\"submit\"\n                  value=\"login\"\n                  disabled={loginPending}\n                >\n                  Log In\n                </Button>\n              </CardFooter>\n            </form>\n          )}\n          {no_auth_configured && (\n            <CardContent className=\"w-full gap-2 text-muted-foreground text-sm\">\n              No login methods have been configured. See the\n              <a\n                href=\"https://github.com/moghtech/komodo/blob/main/config/core.config.toml\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                className=\"text-sm py-0 px-1 underline\"\n              >\n                example config\n              </a>\n              for information on configuring auth.\n            </CardContent>\n          )}\n        </Card>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/resource-notifications.tsx",
    "content": "import { AlertLevel } from \"@components/alert\";\nimport { UpdateDetails } from \"@components/updates/details\";\nimport { Types } from \"komodo_client\";\nimport { ColorIntention, text_color_class_by_intention } from \"@lib/color\";\nimport { fmt_operation, fmt_version, fmt_date } from \"@lib/formatting\";\nimport { useRead } from \"@lib/hooks\";\nimport { getUpdateQuery, cn, version_is_none } from \"@lib/utils\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@ui/tabs\";\nimport {\n  ExternalLink,\n  Check,\n  X,\n  Loader2,\n  Milestone,\n  Calendar,\n  Clock,\n} from \"lucide-react\";\nimport { Link } from \"react-router-dom\";\n\nexport const ResourceNotifications = ({ type, id }: Types.ResourceTarget) => {\n  const deployments = useRead(\"ListDeployments\", {}).data;\n\n  const updates = useRead(\"ListUpdates\", {\n    query: getUpdateQuery({ type, id }, deployments),\n  }).data;\n\n  const alerts = useRead(\"ListAlerts\", {\n    query: getUpdateQuery({ type, id }, deployments),\n  }).data;\n  const openAlerts = alerts?.alerts.filter((alert) => !alert.resolved);\n\n  const showAlerts = type === \"Server\";\n\n  return (\n    <div className=\"shrink-0 p-6 pt-5 pr-3 border rounded-md w-full xl:max-w-[500px]\">\n      <Tabs\n        defaultValue={showAlerts && openAlerts?.length ? \"alerts\" : \"updates\"}\n      >\n        <TabsList>\n          <TabsTrigger value=\"updates\">Updates</TabsTrigger>\n          {showAlerts && <TabsTrigger value=\"alerts\">Alerts</TabsTrigger>}\n        </TabsList>\n        <div className=\"mt-2 pr-3 h-[180px] overflow-y-scroll\">\n          <TabsContent value=\"updates\">\n            {updates?.updates.slice(0, 10).map((update) => (\n              <Update key={update.id} update={update} />\n            ))}\n            <ShowAll to={`/updates?type=${type}&id=${id}`} />\n          </TabsContent>\n          <TabsContent value=\"alerts\">\n            {openAlerts && openAlerts.length ? (\n              openAlerts\n                .slice(0, 10)\n                .map((alert) => <Alert alert={alert} key={alert._id?.$oid} />)\n            ) : (\n              <p className=\"pl-2 text-sm text-muted-foreground\">\n                No open alerts\n              </p>\n            )}\n            <ShowAll to={`/updates?type=${type}&id=${id}`} />\n          </TabsContent>\n        </div>\n      </Tabs>\n    </div>\n  );\n};\n\nconst ShowAll = ({ to }: { to: string }) => (\n  <Link\n    to={to}\n    className=\"mt-2 p-2 border rounded-md flex items-center justify-center text-muted-foreground border-dashed\"\n  >\n    Show All <ExternalLink className=\"w-4 ml-4\" />\n  </Link>\n);\n\nconst Update = ({ update }: { update: Types.UpdateListItem }) => {\n  const intent: ColorIntention =\n    update.status === Types.UpdateStatus.Complete\n      ? update.success\n        ? \"Good\"\n        : \"Critical\"\n      : \"None\";\n\n  const color = text_color_class_by_intention(intent);\n\n  const Icon = () =>\n    update.status === Types.UpdateStatus.Complete ? (\n      update.success ? (\n        <Check className=\"w-4\" />\n      ) : (\n        <X className=\"w-4\" />\n      )\n    ) : (\n      <Loader2 className=\"w-4 animate-spin\" />\n    );\n\n  return (\n    <UpdateDetails id={update.id}>\n      <div className=\"p-2 flex items-center justify-between gap-4 odd:bg-accent/25 hover:bg-accent cursor-pointer\">\n        <div\n          className={cn(\n            \"w-full flex items-center gap-2 text-sm font-bold\",\n            color\n          )}\n        >\n          <Icon />\n          {fmt_operation(update.operation)}\n        </div>\n\n        <div className=\"flex items-center gap-8 shrink-0\">\n          {!version_is_none(update.version) && (\n            <div className=\"flex items-center gap-2 w-full text-xs text-muted-foreground\">\n              <Milestone className=\"w-4\" />\n              {fmt_version(update.version)}\n            </div>\n          )}\n          <div className=\"flex items-center gap-2 text-xs text-muted-foreground text-right shrink-0\">\n            <Calendar className=\"w-4\" />\n            {fmt_date(new Date(update.start_ts))}\n          </div>\n        </div>\n      </div>\n    </UpdateDetails>\n  );\n};\n\nconst Alert = ({ alert }: { alert: Types.Alert }) => (\n  <div className=\"p-2 flex items-center justify-between gap-4 odd:bg-accent/25 hover:bg-accent cursor-pointer\">\n    <AlertLevel level={alert.level} />\n    <div className=\"w-full font-bold max-w-[40%] overflow-hidden overflow-ellipsis\">\n      {alert.data.type}\n    </div>\n    <div className=\"w-fit flex items-center gap-2 text-xs text-muted-foreground text-right shrink-0\">\n      <Clock className=\"w-4\" />\n      {new Date(alert.ts).toLocaleString()}\n    </div>\n  </div>\n);\n"
  },
  {
    "path": "frontend/src/pages/resource.tsx",
    "content": "import { ExportButton } from \"@components/export\";\nimport { Section } from \"@components/layouts\";\nimport { ResourceComponents } from \"@components/resources\";\nimport {\n  CopyResource,\n  ResourceDescription,\n} from \"@components/resources/common\";\nimport { AddTags, ResourceTags } from \"@components/tags\";\nimport {\n  usePermissions,\n  usePushRecentlyViewed,\n  useRead,\n  useResourceParamType,\n  useSetTitle,\n} from \"@lib/hooks\";\nimport { SETTINGS_RESOURCES, usableResourcePath } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { UsableResource } from \"@types\";\nimport { Button } from \"@ui/button\";\nimport { AlertTriangle, ChevronLeft, LinkIcon, Zap } from \"lucide-react\";\nimport { Link, useParams } from \"react-router-dom\";\nimport { ResourceNotifications } from \"./resource-notifications\";\nimport { NotFound } from \"@components/util\";\n\nexport default function Resource() {\n  const type = useResourceParamType()!;\n  const id = useParams().id as string;\n\n  if (!type || !id) return null;\n\n  return <ResourceInner type={type} id={id} />;\n}\n\nconst ResourceInner = ({ type, id }: { type: UsableResource; id: string }) => {\n  const resources = useRead(`List${type}s`, {}).data;\n  const resource = resources?.find((resource) => resource.id === id);\n  const full_resource = useRead(`Get${type}`, { id } as any).data;\n\n  usePushRecentlyViewed({ type, id });\n\n  const { canCreate, canExecute, canWrite } = usePermissions({ type, id });\n\n  if (!type || !id) return null;\n\n  if (!resource) {\n    if (resources) return <NotFound type={type} />;\n    else return null;\n  }\n\n  let showExport = true;\n  if (type === \"ResourceSync\") {\n    const info = resource?.info as Types.ResourceSyncListItemInfo;\n    showExport = !info?.file_contents && (info.file_contents || !info.managed);\n  }\n\n  const Components = ResourceComponents[type];\n  const links = full_resource ? Components.resource_links(full_resource) : [];\n\n  return (\n    <div>\n      <div className=\"w-full flex items-center justify-between mb-12\">\n        <Link\n          to={\n            \"/\" +\n            (SETTINGS_RESOURCES.includes(type)\n              ? \"settings\"\n              : usableResourcePath(type))\n          }\n        >\n          <Button className=\"gap-2\" variant=\"secondary\">\n            <ChevronLeft className=\"w-4\" />\n            Back\n          </Button>\n        </Link>\n        <div className=\"flex items-center gap-4\">\n          {type !== \"Server\" && canCreate && (\n            <CopyResource type={type} id={id} />\n          )}\n          {showExport && <ExportButton targets={[{ type, id }]} />}\n        </div>\n      </div>\n      <div className=\"flex flex-col xl:flex-row gap-4\">\n        <ResourceHeader type={type} id={id} links={links} />\n        <ResourceNotifications type={type} id={id} />\n      </div>\n      <div className=\"mt-8 flex flex-col gap-12\">\n        {canExecute && Object.keys(Components.Actions).length > 0 && (\n          <Section title=\"Execute\" icon={<Zap className=\"w-4 h-4\" />}>\n            <div className=\"flex gap-4 items-center flex-wrap\">\n              {Object.entries(Components.Actions).map(([key, Action]) => (\n                <Action key={key} id={id} />\n              ))}\n            </div>\n          </Section>\n        )}\n        {Object.entries(Components.Page).map(([key, Component]) => (\n          <Component key={key} id={id} />\n        ))}\n        <Components.Config id={id} />\n        {canWrite && (\n          <Section\n            title=\"Danger Zone\"\n            icon={<AlertTriangle className=\"w-6 h-6\" />}\n            // actions={\n            // type !== \"Server\" &&\n            // canCreate && <CopyResource type={type} id={id} />\n            // }\n          >\n            <Components.DangerZone id={id} />\n          </Section>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport const ResourceHeader = ({\n  type,\n  id,\n  links,\n}: {\n  type: UsableResource;\n  id: string;\n  links: string[] | undefined;\n}) => {\n  const resource = useRead(`List${type}s`, {}).data?.find((r) => r.id === id);\n  useSetTitle(resource?.name);\n\n  const Components = ResourceComponents[type];\n  const infoEntries = Object.entries(Components.Info);\n  const statusEntries = Object.entries(Components.Status);\n\n  const { canWrite } = usePermissions({ type, id });\n\n  return (\n    <div className=\"w-full flex flex-col gap-4\">\n      <div className=\"flex flex-col gap-4 border rounded-md\">\n        <Components.ResourcePageHeader id={id} />\n        <div className=\"flex items-center gap-x-4 gap-y-2 flex-wrap px-4 py-0\">\n          {infoEntries.map(([key, Info]) => (\n            <div key={key} className=\"pr-4 text-sm border-r\">\n              <Info id={id} />\n            </div>\n          ))}\n          {statusEntries.map(([key, Status]) => (\n            <Status key={key} id={id} />\n          ))}\n        </div>\n        {links && links.length > 0 && (\n          <div className=\"flex items-center gap-x-4 gap-y-2 flex-wrap px-4 py-0\">\n            {links?.map((link) => (\n              <a\n                key={link}\n                target=\"_blank\"\n                href={link}\n                className=\"flex gap-2 items-center pr-4 text-sm border-r cursor-pointer hover:underline last:pr-0 last:border-none\"\n              >\n                <LinkIcon className=\"w-4\" />\n                <div className=\"max-w-[150px] lg:max-w-[250px] text-nowrap overflow-hidden overflow-ellipsis\">\n                  {link}\n                </div>\n              </a>\n            ))}\n          </div>\n        )}\n        <div className=\"flex items-center gap-2 flex-wrap p-4 pt-0\">\n          <p className=\"text-sm text-muted-foreground\">Tags:</p>\n          <ResourceTags\n            target={{ id, type }}\n            className=\"text-sm\"\n            disabled={!canWrite}\n            click_to_delete\n          />\n          {canWrite && <AddTags target={{ id, type }} />}\n        </div>\n      </div>\n      <ResourceDescription type={type} id={id} disabled={!canWrite} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/resources.tsx",
    "content": "import { ExportButton } from \"@components/export\";\nimport { Page } from \"@components/layouts\";\nimport { ResourceComponents } from \"@components/resources\";\nimport { TagsFilter } from \"@components/tags\";\nimport {\n  useFilterByUpdateAvailable,\n  useFilterResources,\n  useRead,\n  useResourceParamType,\n  useSetTitle,\n  useTemplatesQueryBehavior,\n  useUser,\n  useLocalStorage,\n} from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Input } from \"@ui/input\";\nimport { Button } from \"@ui/button\";\nimport { useState } from \"react\";\nimport { Search, Eye, EyeOff } from \"lucide-react\";\nimport { NotFound, TemplateQueryBehaviorSelector } from \"@components/util\";\nimport { Switch } from \"@ui/switch\";\nimport { UsableResource } from \"@types\";\nimport { ServerMonitoringTable } from \"@components/resources/server/monitoring-table\";\n\nexport default function Resources({ _type }: { _type?: UsableResource }) {\n  const is_admin = useUser().data?.admin ?? false;\n  const disable_non_admin_create =\n    useRead(\"GetCoreInfo\", {}).data?.disable_non_admin_create ?? true;\n  const __type = useResourceParamType()!;\n  const type = _type ? _type : __type;\n  const name = type === \"ResourceSync\" ? \"Resource Sync\" : type;\n  useSetTitle(name + \"s\");\n  const [search, set] = useState(\"\");\n  const [monitoring, setMonitoring] = useLocalStorage<boolean>(\n    \"servers-monitoring-toggle-v1\",\n    false\n  );\n  const [filter_update_available, toggle_filter_update_available] =\n    useFilterByUpdateAvailable();\n  const query =\n    type === \"Stack\" || type === \"Deployment\"\n      ? {\n          query: {\n            specific: { update_available: filter_update_available },\n          },\n        }\n      : {};\n  const [templatesQueryBehavior] = useTemplatesQueryBehavior();\n  const resources = useRead(`List${type}s`, query).data;\n  const templatesFilterFn =\n    templatesQueryBehavior === Types.TemplatesQueryBehavior.Exclude\n      ? (resource: Types.ResourceListItem<unknown>) => !resource.template\n      : templatesQueryBehavior === Types.TemplatesQueryBehavior.Only\n        ? (resource: Types.ResourceListItem<unknown>) => resource.template\n        : () => true;\n  const filtered = useFilterResources(resources as any, search).filter(\n    templatesFilterFn\n  );\n\n  const Components = ResourceComponents[type];\n\n  if (!Components) {\n    return <NotFound type={undefined} />;\n  }\n\n  const targets = filtered?.map((resource) => ({ type, id: resource.id }));\n\n  return (\n    <Page\n      title={`${name}s`}\n      subtitle={\n        <div className=\"text-muted-foreground\">\n          <Components.Description />\n        </div>\n      }\n      icon={<Components.BigIcon />}\n      actions={\n        <div className=\"flex items-center gap-2\">\n          {type === \"Server\" && (\n            <Button\n              variant=\"outline\"\n              className=\"flex items-center gap-2\"\n              onClick={() => setMonitoring(!monitoring)}\n            >\n              {monitoring ? (\n                <>\n                  <EyeOff className=\"w-4 h-4\" />\n                  Hide Server Stats\n                </>\n              ) : (\n                <>\n                  <Eye className=\"w-4 h-4\" />\n                  Show Server Stats\n                </>\n              )}\n            </Button>\n          )}\n          <ExportButton targets={targets} />\n        </div>\n      }\n    >\n      <div className=\"flex flex-col gap-4\">\n        <div className=\"flex flex-wrap gap-4 items-center justify-between\">\n          <div className=\"flex gap-4\">\n            {(is_admin || !disable_non_admin_create) && <Components.New />}\n            {!(type === \"Server\" && monitoring) && <Components.GroupActions />}\n          </div>\n          <div className=\"flex items-center gap-4 flex-wrap\">\n            {(type === \"Stack\" || type === \"Deployment\") && (\n              <div\n                className=\"flex gap-2 items-center cursor-pointer px-3 py-2 text-sm text-muted-foreground\"\n                onClick={() => toggle_filter_update_available()}\n              >\n                Pending Update\n                <Switch checked={filter_update_available} />\n              </div>\n            )}\n            {!(type === \"Server\" && monitoring) && (\n              <TemplateQueryBehaviorSelector />\n            )}\n            {!(type === \"Server\" && monitoring) && <TagsFilter />}\n            <div className=\"relative\">\n              <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n              <Input\n                value={search}\n                onChange={(e) => set(e.target.value)}\n                placeholder=\"search...\"\n                className=\"pl-8 w-[200px] lg:w-[300px]\"\n              />\n            </div>\n          </div>\n        </div>\n        {type === \"Server\" && monitoring ? (\n          <ServerMonitoringTable search={search} />\n        ) : (\n          <Components.Table resources={filtered ?? []} />\n        )}\n      </div>\n    </Page>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/schedules.tsx",
    "content": "import { Page } from \"@components/layouts\";\nimport { ResourceLink } from \"@components/resources/common\";\nimport { TableTags, TagsFilter } from \"@components/tags\";\nimport {\n  usePermissions,\n  useRead,\n  useSetTitle,\n  useTags,\n  useWrite,\n} from \"@lib/hooks\";\nimport { filterBySplit } from \"@lib/utils\";\nimport { UsableResource } from \"@types\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport { Input } from \"@ui/input\";\nimport { Switch } from \"@ui/switch\";\nimport { useToast } from \"@ui/use-toast\";\nimport { CalendarDays, Search } from \"lucide-react\";\nimport { useState } from \"react\";\n\nexport default function SchedulesPage() {\n  useSetTitle(\"Schedules\");\n  const [search, set] = useState(\"\");\n  const { tags } = useTags();\n  const schedules = useRead(\"ListSchedules\", { tags }).data;\n  const filtered = filterBySplit(schedules ?? [], search, (item) => item.name);\n  return (\n    <Page\n      icon={<CalendarDays className=\"w-8\" />}\n      title=\"Schedules\"\n      subtitle={\n        <div className=\"text-muted-foreground\">\n          See an overview of your scheduled tasks.\n        </div>\n      }\n    >\n      <div className=\"flex flex-col gap-4\">\n        <div className=\"flex flex-wrap gap-4 items-center justify-end\">\n          <div className=\"flex items-center gap-4 flex-wrap\">\n            <TagsFilter />\n            <div className=\"relative\">\n              <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n              <Input\n                value={search}\n                onChange={(e) => set(e.target.value)}\n                placeholder=\"search...\"\n                className=\"pl-8 w-[200px] lg:w-[300px]\"\n              />\n            </div>\n          </div>\n        </div>\n        <DataTable\n          tableKey=\"schedules\"\n          data={filtered}\n          columns={[\n            {\n              size: 200,\n              accessorKey: \"name\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Target\" />\n              ),\n              cell: ({ row }) => (\n                <ResourceLink\n                  type={row.original.target.type as UsableResource}\n                  id={row.original.target.id}\n                />\n              ),\n            },\n            {\n              size: 200,\n              accessorKey: \"schedule\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Schedule\" />\n              ),\n            },\n            {\n              size: 200,\n              accessorKey: \"next_scheduled_run\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Next Run\" />\n              ),\n              sortingFn: (a, b) => {\n                const sa = a.original.next_scheduled_run;\n                const sb = b.original.next_scheduled_run;\n\n                if (!sa && !sb) return 0;\n                if (!sa) return 1;\n                if (!sb) return -1;\n\n                if (sa > sb) return 1;\n                else if (sa < sb) return -1;\n                else return 0;\n              },\n              cell: ({ row }) =>\n                row.original.next_scheduled_run\n                  ? new Date(row.original.next_scheduled_run).toLocaleString()\n                  : \"Not Scheduled\",\n            },\n            {\n              size: 100,\n              accessorKey: \"enabled\",\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Enabled\" />\n              ),\n              cell: ({ row: { original: schedule } }) => (\n                <ScheduleEnableSwitch\n                  type={schedule.target.type as UsableResource}\n                  id={schedule.target.id}\n                  enabled={schedule.enabled}\n                />\n              ),\n            },\n            {\n              header: \"Tags\",\n              cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,\n            },\n          ]}\n        />\n      </div>\n    </Page>\n  );\n}\n\nconst ScheduleEnableSwitch = ({\n  type,\n  id,\n  enabled,\n}: {\n  type: UsableResource;\n  id: string;\n  enabled: boolean;\n}) => {\n  const { canWrite } = usePermissions({ type, id });\n  const { toast } = useToast();\n  const { mutate } = useWrite(`Update${type}`, {\n    onSuccess: () => toast({ title: \"Updated Schedule enabled.\" }),\n  });\n  return (\n    <Switch\n      checked={enabled}\n      onCheckedChange={(enabled) =>\n        mutate({ id, config: { schedule_enabled: enabled } })\n      }\n      disabled={!canWrite}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/server-info/container/actions.tsx",
    "content": "import { ActionWithDialog, ConfirmButton } from \"@components/util\";\nimport { useExecute, useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Pause, Play, RefreshCcw, Square, Trash } from \"lucide-react\";\nimport { useNavigate } from \"react-router-dom\";\n\nconst useContainer = (id: string, container_name: string) => {\n  return useRead(\"ListDockerContainers\", { server: id }).data?.find(\n    (container) => container.name === container_name\n  );\n};\n\nconst DestroyContainer = ({\n  id,\n  container: container_name,\n}: {\n  id: string;\n  container: string;\n}) => {\n  const container = useContainer(id, container_name);\n  const nav = useNavigate();\n  const { mutate: destroy, isPending } = useExecute(\"DestroyContainer\", {\n    onSuccess: () => nav(\"/servers/\" + id),\n  });\n  const destroying = useRead(\n    \"GetServerActionState\",\n    { server: id },\n    { refetchInterval: 5000 }\n  ).data?.pruning_containers;\n\n  if (!container) {\n    return null;\n  }\n\n  return (\n    <ActionWithDialog\n      name={container_name}\n      title=\"Destroy\"\n      icon={<Trash className=\"h-4 w-4\" />}\n      onClick={() => destroy({ server: id, container: container_name })}\n      disabled={isPending}\n      loading={isPending || destroying}\n    />\n  );\n};\n\nconst RestartContainer = ({\n  id,\n  container: container_name,\n}: {\n  id: string;\n  container: string;\n}) => {\n  const container = useContainer(id, container_name);\n  const state = container?.state;\n  const { mutate: restart, isPending: restartPending } =\n    useExecute(\"RestartContainer\");\n  const action_state = useRead(\n    \"GetServerActionState\",\n    { server: id },\n    { refetchInterval: 5000 }\n  ).data;\n\n  if (!container || state !== Types.ContainerStateStatusEnum.Running) {\n    return null;\n  }\n\n  return (\n    <ActionWithDialog\n      name={container_name}\n      title=\"Restart\"\n      icon={<RefreshCcw className=\"h-4 w-4\" />}\n      onClick={() => restart({ server: id, container: container_name })}\n      disabled={restartPending}\n      loading={restartPending || action_state?.restarting_containers}\n    />\n  );\n};\n\nconst StartStopContainer = ({\n  id,\n  container: container_name,\n}: {\n  id: string;\n  container: string;\n}) => {\n  const container = useContainer(id, container_name);\n  const state = container?.state;\n  const { mutate: start, isPending: startPending } =\n    useExecute(\"StartContainer\");\n  const { mutate: stop, isPending: stopPending } = useExecute(\"StopContainer\");\n  const action_state = useRead(\n    \"GetServerActionState\",\n    { server: id },\n    { refetchInterval: 5000 }\n  ).data;\n\n  if (!container) {\n    return null;\n  }\n\n  if (state === Types.ContainerStateStatusEnum.Exited) {\n    return (\n      <ConfirmButton\n        title=\"Start\"\n        icon={<Play className=\"h-4 w-4\" />}\n        onClick={() => start({ server: id, container: container_name })}\n        disabled={startPending}\n        loading={startPending || action_state?.starting_containers}\n      />\n    );\n  }\n  if (state === Types.ContainerStateStatusEnum.Running) {\n    return (\n      <ActionWithDialog\n        name={container_name}\n        title=\"Stop\"\n        icon={<Square className=\"h-4 w-4\" />}\n        onClick={() => stop({ server: id, container: container_name })}\n        disabled={stopPending}\n        loading={stopPending || action_state?.stopping_containers}\n      />\n    );\n  }\n};\n\nconst PauseUnpauseContainer = ({\n  id,\n  container: container_name,\n}: {\n  id: string;\n  container: string;\n}) => {\n  const container = useContainer(id, container_name);\n  const state = container?.state;\n  const { mutate: unpause, isPending: unpausePending } =\n    useExecute(\"UnpauseContainer\");\n  const { mutate: pause, isPending: pausePending } =\n    useExecute(\"PauseContainer\");\n  const action_state = useRead(\n    \"GetServerActionState\",\n    { server: id },\n    { refetchInterval: 5000 }\n  ).data;\n\n  if (!container) {\n    return null;\n  }\n\n  if (state === Types.ContainerStateStatusEnum.Paused) {\n    return (\n      <ConfirmButton\n        title=\"Unpause\"\n        icon={<Play className=\"h-4 w-4\" />}\n        onClick={() => unpause({ server: id, container: container_name })}\n        disabled={unpausePending}\n        loading={unpausePending || action_state?.unpausing_containers}\n      />\n    );\n  }\n  if (state === Types.ContainerStateStatusEnum.Running) {\n    return (\n      <ActionWithDialog\n        name={container_name}\n        title=\"Pause\"\n        icon={<Pause className=\"h-4 w-4\" />}\n        onClick={() => pause({ server: id, container: container_name })}\n        disabled={pausePending}\n        loading={pausePending || action_state?.pausing_containers}\n      />\n    );\n  }\n};\n\ntype IdContainerComponent = React.FC<{ id: string; container: string }>;\n\nexport const Actions: { [action: string]: IdContainerComponent } = {\n  RestartContainer,\n  PauseUnpauseContainer,\n  StartStopContainer,\n  DestroyContainer,\n};\n"
  },
  {
    "path": "frontend/src/pages/server-info/container/index.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { ResourceLink, ResourcePageHeader } from \"@components/resources/common\";\nimport { useServer } from \"@components/resources/server\";\nimport {\n  ConfirmButton,\n  ContainerPortLink,\n  DOCKER_LINK_ICONS,\n  DockerLabelsSection,\n  DockerResourceLink,\n} from \"@components/util\";\nimport {\n  useContainerPortsMap,\n  useLocalStorage,\n  useRead,\n  useSetTitle,\n  useWrite,\n} from \"@lib/hooks\";\nimport { Button } from \"@ui/button\";\nimport { DataTable } from \"@ui/data-table\";\nimport {\n  ChevronLeft,\n  Clapperboard,\n  Info,\n  Loader2,\n  PlusCircle,\n} from \"lucide-react\";\nimport { Link, useNavigate, useParams } from \"react-router-dom\";\nimport { ContainerLogs } from \"./log\";\nimport { Actions } from \"./actions\";\nimport { ConnectExecQuery, Types } from \"komodo_client\";\nimport { container_state_intention } from \"@lib/color\";\nimport { UsableResource } from \"@types\";\nimport { Fragment } from \"react/jsx-runtime\";\nimport { usePermissions } from \"@lib/hooks\";\nimport { ResourceNotifications } from \"@pages/resource-notifications\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@ui/tabs\";\nimport { ContainerTerminal } from \"@components/terminal/container\";\nimport { ContainerInspect } from \"./inspect\";\nimport { useMemo } from \"react\";\n\nexport default function ContainerPage() {\n  const { type, id, container } = useParams() as {\n    type: string;\n    id: string;\n    container: string;\n  };\n  if (type !== \"servers\") {\n    return <div>This resource type does not have any containers.</div>;\n  }\n  return (\n    <ContainerPageInner id={id} container={decodeURIComponent(container)} />\n  );\n}\n\nconst ContainerPageInner = ({\n  id,\n  container: container_name,\n}: {\n  id: string;\n  container: string;\n}) => {\n  const server = useServer(id);\n  useSetTitle(`${server?.name} | container | ${container_name}`);\n  const { canExecute } = usePermissions({ type: \"Server\", id });\n  const list_container = useRead(\n    \"ListDockerContainers\",\n    {\n      server: id,\n    },\n    { refetchInterval: 10_000 }\n  ).data?.find((container) => container.name === container_name);\n  const ports_map = useContainerPortsMap(list_container?.ports ?? []);\n\n  const state = list_container?.state ?? Types.ContainerStateStatusEnum.Empty;\n  const intention = container_state_intention(state);\n\n  return (\n    <div>\n      <div className=\"w-full flex items-center justify-between mb-12\">\n        <Link to={\"/servers/\" + id}>\n          <Button className=\"gap-2\" variant=\"secondary\">\n            <ChevronLeft className=\"w-4\" />\n            Back\n          </Button>\n        </Link>\n        <NewDeployment id={id} name={container_name} />\n      </div>\n      <div className=\"flex flex-col xl:flex-row gap-4\">\n        {/** HEADER */}\n        <div className=\"w-full flex flex-col gap-4\">\n          <div className=\"flex flex-col gap-2 border rounded-md\">\n            {/* <Components.ResourcePageHeader id={id} /> */}\n            <ResourcePageHeader\n              type={undefined}\n              id={undefined}\n              intent={intention}\n              icon={\n                <DOCKER_LINK_ICONS.container\n                  server_id={id}\n                  name={container_name}\n                  size={8}\n                />\n              }\n              resource={undefined}\n              name={container_name}\n              state={state}\n              status={list_container?.status}\n            />\n            <div className=\"flex flex-col pb-2 px-4\">\n              <div className=\"flex items-center gap-x-4 gap-y-1 flex-wrap text-muted-foreground\">\n                <ResourceLink type=\"Server\" id={id} />\n                <AttachedResource id={id} container={container_name} />\n                {list_container?.image && (\n                  <>\n                    |\n                    <DockerResourceLink\n                      type=\"image\"\n                      server_id={id}\n                      name={list_container.image}\n                      id={list_container.image_id}\n                      muted\n                    />\n                  </>\n                )}\n                {list_container?.networks?.map((network) => (\n                  <Fragment key={network}>\n                    |\n                    <DockerResourceLink\n                      type=\"network\"\n                      server_id={id}\n                      name={network}\n                      muted\n                    />\n                  </Fragment>\n                ))}\n                {list_container?.volumes?.map((volume) => (\n                  <Fragment key={volume}>\n                    |\n                    <DockerResourceLink\n                      type=\"volume\"\n                      server_id={id}\n                      name={volume}\n                      muted\n                    />\n                  </Fragment>\n                ))}\n                {Object.keys(ports_map).map((host_port) => (\n                  <Fragment key={host_port}>\n                    |\n                    <ContainerPortLink\n                      host_port={host_port}\n                      ports={ports_map[host_port]}\n                      server_id={id}\n                    />\n                  </Fragment>\n                ))}\n              </div>\n            </div>\n          </div>\n          {/* <ResourceDescription type=\"Server\" id={id} disabled={!canWrite} /> */}\n        </div>\n        {/** NOTIFICATIONS */}\n        <ResourceNotifications type=\"Server\" id={id} />\n      </div>\n\n      <div className=\"mt-8 flex flex-col gap-12\">\n        {/* Actions */}\n        {canExecute && (\n          <Section title=\"Actions\" icon={<Clapperboard className=\"w-4 h-4\" />}>\n            <div className=\"flex gap-4 items-center flex-wrap\">\n              {Object.entries(Actions).map(([key, Action]) => (\n                <Action key={key} id={id} container={container_name} />\n              ))}\n            </div>\n          </Section>\n        )}\n\n        <ContainerTabs server={id} container={container_name} state={state} />\n\n        {/* TOP LEVEL CONTAINER INFO */}\n        {list_container && (\n          <Section title=\"Details\" icon={<Info className=\"w-4 h-4\" />}>\n            <DataTable\n              tableKey=\"container-info\"\n              data={[list_container]}\n              columns={[\n                {\n                  header: \"Id\",\n                  accessorKey: \"id\",\n                },\n                {\n                  header: \"Image\",\n                  accessorKey: \"image\",\n                },\n                {\n                  header: \"Network Mode\",\n                  accessorKey: \"network_mode\",\n                },\n                {\n                  header: \"Networks\",\n                  accessorKey: \"networks\",\n                },\n              ]}\n            />\n          </Section>\n        )}\n\n        <DockerLabelsSection labels={list_container?.labels} />\n      </div>\n    </div>\n  );\n};\n\nconst ContainerTabs = ({\n  server,\n  container,\n  state,\n}: {\n  server: string;\n  container: string;\n  state: Types.ContainerStateStatusEnum;\n}) => {\n  const [_view, setView] = useLocalStorage<\"Log\" | \"Inspect\" | \"Terminal\">(\n    `server-${server}-${container}-tabs-v1`,\n    \"Log\"\n  );\n  const { specificLogs, specificInspect, specificTerminal } = usePermissions({\n    type: \"Server\",\n    id: server,\n  });\n  const container_exec_disabled =\n    useServer(server)?.info.container_exec_disabled ?? true;\n  const logDisabled =\n    !specificLogs || state === Types.ContainerStateStatusEnum.Empty;\n  const inspectDisabled =\n    !specificInspect || state === Types.ContainerStateStatusEnum.Empty;\n  const terminalDisabled =\n    !specificTerminal ||\n    container_exec_disabled ||\n    state !== Types.ContainerStateStatusEnum.Running;\n  const view =\n    (inspectDisabled && _view === \"Inspect\") ||\n    (terminalDisabled && _view === \"Terminal\")\n      ? \"Log\"\n      : _view;\n  const tabs = useMemo(() => {\n    return (\n      <TabsList className=\"justify-start w-fit\">\n        <TabsTrigger value=\"Log\" className=\"w-[110px]\" disabled={logDisabled}>\n          Log\n        </TabsTrigger>\n        {specificInspect && (\n          <TabsTrigger\n            value=\"Inspect\"\n            className=\"w-[110px]\"\n            disabled={inspectDisabled}\n          >\n            Inspect\n          </TabsTrigger>\n        )}\n        {specificTerminal && (\n          <TabsTrigger\n            value=\"Terminal\"\n            className=\"w-[110px]\"\n            disabled={terminalDisabled}\n          >\n            Terminal\n          </TabsTrigger>\n        )}\n      </TabsList>\n    );\n  }, [\n    logDisabled,\n    specificInspect,\n    inspectDisabled,\n    specificTerminal,\n    terminalDisabled,\n  ]);\n  const terminalQuery = useMemo(\n    () =>\n      ({\n        type: \"container\",\n        query: {\n          server,\n          container,\n          // This is handled inside ContainerTerminal\n          shell: \"\",\n        },\n      }) as ConnectExecQuery,\n    [server, container]\n  );\n  return (\n    <Tabs value={view} onValueChange={setView as any}>\n      <TabsContent value=\"Log\">\n        <ContainerLogs\n          id={server}\n          container_name={container}\n          titleOther={tabs}\n          disabled={logDisabled}\n        />\n      </TabsContent>\n      <TabsContent value=\"Inspect\">\n        <ContainerInspect id={server} container={container} titleOther={tabs} />\n      </TabsContent>\n      <TabsContent value=\"Terminal\">\n        <ContainerTerminal query={terminalQuery} titleOther={tabs} />\n      </TabsContent>\n    </Tabs>\n  );\n};\n\nconst AttachedResource = ({\n  id,\n  container,\n}: {\n  id: string;\n  container: string;\n}) => {\n  const { data: attached, isPending } = useRead(\n    \"GetResourceMatchingContainer\",\n    { server: id, container },\n    { refetchInterval: 10_000 }\n  );\n\n  if (isPending) {\n    return <Loader2 className=\"w-4 h-4 animate-spin\" />;\n  }\n\n  if (!attached || !attached.resource) {\n    return null;\n  }\n\n  return (\n    <>\n      |\n      <ResourceLink\n        type={attached.resource.type as UsableResource}\n        id={attached.resource.id}\n      />\n    </>\n  );\n};\n\nconst NewDeployment = ({ id, name }: { id: string; name: string }) => {\n  const { data: attached, isPending } = useRead(\n    \"GetResourceMatchingContainer\",\n    { server: id, container: name }\n  );\n\n  if (isPending) {\n    return <Loader2 className=\"w-4 h-4 animate-spin\" />;\n  }\n\n  if (!attached) {\n    return null;\n  }\n\n  if (!attached?.resource) {\n    return <NewDeploymentInner name={name} server_id={id} />;\n  }\n};\n\nconst NewDeploymentInner = ({\n  server_id,\n  name,\n}: {\n  name: string;\n  server_id: string;\n}) => {\n  const nav = useNavigate();\n  const { mutateAsync, isPending } = useWrite(\"CreateDeploymentFromContainer\");\n  return (\n    <ConfirmButton\n      title=\"New Deployment\"\n      icon={<PlusCircle className=\"w-4 h-4\" />}\n      onClick={async () => {\n        const id = (await mutateAsync({ name, server: server_id }))._id?.$oid!;\n        nav(`/deployments/${id}`);\n      }}\n      loading={isPending}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/server-info/container/inspect.tsx",
    "content": "import { usePermissions, useRead } from \"@lib/hooks\";\nimport { ReactNode } from \"react\";\nimport { Types } from \"komodo_client\";\nimport { Section } from \"@components/layouts\";\nimport { InspectContainerView } from \"@components/inspect\";\n\nexport const ContainerInspect = ({\n  id,\n  container,\n  titleOther,\n}: {\n  id: string;\n  container: string;\n  titleOther: ReactNode;\n}) => {\n  const { specific } = usePermissions({ type: \"Server\", id });\n  if (!specific.includes(Types.SpecificPermission.Inspect)) {\n    return (\n      <Section titleOther={titleOther}>\n        <div className=\"min-h-[60vh]\">\n          <h1>User does not have permission to inspect this Server.</h1>\n        </div>\n      </Section>\n    );\n  }\n  return (\n    <Section titleOther={titleOther}>\n      <ContainerInspectInner id={id} container={container} />\n    </Section>\n  );\n};\n\nconst ContainerInspectInner = ({\n  id,\n  container,\n}: {\n  id: string;\n  container: string;\n}) => {\n  const {\n    data: inspect_container,\n    error,\n    isPending,\n    isError,\n  } = useRead(\"InspectDockerContainer\", {\n    server: id,\n    container,\n  });\n  return (\n    <InspectContainerView\n      container={inspect_container}\n      error={error}\n      isPending={isPending}\n      isError={isError}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/server-info/container/log.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { Log, LogSection } from \"@components/log\";\nimport { useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { ReactNode } from \"react\";\n\nexport const ContainerLogs = ({\n  id,\n  container_name,\n  titleOther,\n  disabled,\n}: {\n  id: string;\n  container_name: string;\n  titleOther?: ReactNode;\n  disabled: boolean;\n}) => {\n  if (disabled) {\n    return (\n      <Section titleOther={titleOther}>\n        <h1>Logs are disabled.</h1>\n      </Section>\n    );\n  }\n  return (\n    <LogSection\n      titleOther={titleOther}\n      regular_logs={(timestamps, stream, tail, poll) =>\n        NoSearchLogs(id, container_name, tail, timestamps, stream, poll)\n      }\n      search_logs={(timestamps, terms, invert, poll) =>\n        SearchLogs(id, container_name, terms, invert, timestamps, poll)\n      }\n    />\n  );\n};\n\nconst NoSearchLogs = (\n  id: string,\n  container: string,\n  tail: number,\n  timestamps: boolean,\n  stream: string,\n  poll: boolean\n) => {\n  const { data: log, refetch } = useRead(\n    \"GetContainerLog\",\n    {\n      server: id,\n      container,\n      tail: Number(tail),\n      timestamps,\n    },\n    { refetchInterval: poll ? 3000 : false }\n  );\n  return {\n    Log: (\n      <div className=\"relative\">\n        <Log log={log} stream={stream as \"stdout\" | \"stderr\"} />\n      </div>\n    ),\n    refetch,\n    stderr: !!log?.stderr,\n  };\n};\n\nconst SearchLogs = (\n  id: string,\n  container: string,\n  terms: string[],\n  invert: boolean,\n  timestamps: boolean,\n  poll: boolean\n) => {\n  const { data: log, refetch } = useRead(\n    \"SearchContainerLog\",\n    {\n      server: id,\n      container,\n      terms,\n      combinator: Types.SearchCombinator.And,\n      invert,\n      timestamps,\n    },\n    { refetchInterval: poll ? 10000 : false }\n  );\n  return {\n    Log: (\n      <div className=\"h-full relative\">\n        <Log log={log} stream=\"stdout\" />\n      </div>\n    ),\n    refetch,\n    stderr: !!log?.stderr,\n  };\n};\n"
  },
  {
    "path": "frontend/src/pages/server-info/image.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { ResourceLink } from \"@components/resources/common\";\nimport { useServer } from \"@components/resources/server\";\nimport {\n  ConfirmButton,\n  DOCKER_LINK_ICONS,\n  DockerContainersSection,\n  DockerLabelsSection,\n  DockerResourcePageName,\n  ShowHideButton,\n} from \"@components/util\";\nimport { fmt_date_with_minutes, format_size_bytes } from \"@lib/formatting\";\nimport { useExecute, usePermissions, useRead, useSetTitle } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Badge } from \"@ui/badge\";\nimport { Button } from \"@ui/button\";\nimport { DataTable } from \"@ui/data-table\";\nimport {\n  ChevronLeft,\n  HistoryIcon,\n  Info,\n  Loader2,\n  SearchCode,\n  Trash,\n} from \"lucide-react\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport { useState } from \"react\";\nimport { MonacoEditor } from \"@components/monaco\";\n\nexport default function ImagePage() {\n  const { type, id, image } = useParams() as {\n    type: string;\n    id: string;\n    image: string;\n  };\n  if (type !== \"servers\") {\n    return <div>This resource type does not have any images.</div>;\n  }\n  return <ImagePageInner id={id} image={decodeURIComponent(image)} />;\n}\n\nconst ImagePageInner = ({\n  id,\n  image: image_name,\n}: {\n  id: string;\n  image: string;\n}) => {\n  const [showInspect, setShowInspect] = useState(false);\n  const server = useServer(id);\n  useSetTitle(`${server?.name} | image | ${image_name}`);\n  const nav = useNavigate();\n\n  const { canExecute, specific } = usePermissions({ type: \"Server\", id });\n\n  const {\n    data: image,\n    isPending,\n    isError,\n  } = useRead(\"InspectDockerImage\", {\n    server: id,\n    image: image_name,\n  });\n\n  const containers = useRead(\n    \"ListDockerContainers\",\n    {\n      server: id,\n    },\n    { refetchInterval: 10_000 }\n  ).data?.filter((container) =>\n    !image?.Id ? false : container.image_id === image?.Id\n  );\n\n  const history = useRead(\"ListDockerImageHistory\", {\n    server: id,\n    image: image_name,\n  }).data;\n\n  const { mutate: deleteImage, isPending: deletePending } = useExecute(\n    \"DeleteImage\",\n    {\n      onSuccess: () => nav(\"/servers/\" + id),\n    }\n  );\n\n  if (isPending) {\n    return (\n      <div className=\"flex justify-center w-full py-4\">\n        <Loader2 className=\"w-8 h-8 animate-spin\" />\n      </div>\n    );\n  }\n\n  if (isError) {\n    return <div className=\"flex w-full py-4\">Failed to inspect image.</div>;\n  }\n\n  if (!image) {\n    return (\n      <div className=\"flex w-full py-4\">\n        No image found with given name: {image_name}\n      </div>\n    );\n  }\n\n  const unused = containers && containers.length === 0 ? true : false;\n\n  return (\n    <div className=\"flex flex-col gap-16 mb-24\">\n      {/* HEADER */}\n      <div className=\"flex flex-col gap-4\">\n        {/* BACK */}\n        <div className=\"flex items-center justify-between mb-4\">\n          <Button\n            className=\"gap-2\"\n            variant=\"secondary\"\n            onClick={() => nav(\"/servers/\" + id)}\n          >\n            <ChevronLeft className=\"w-4\" /> Back\n          </Button>\n        </div>\n\n        {/* TITLE */}\n        <div className=\"flex items-center gap-4\">\n          <div className=\"mt-1\">\n            <DOCKER_LINK_ICONS.image server_id={id} name={image.Id} size={8} />\n          </div>\n          <DockerResourcePageName name={image_name} />\n          {unused && <Badge variant=\"destructive\">Unused</Badge>}\n        </div>\n\n        {/* INFO */}\n        <div className=\"flex flex-wrap gap-4 items-center text-muted-foreground\">\n          <ResourceLink type=\"Server\" id={id} />\n          {image.Id ? (\n            <>\n              |\n              <div className=\"flex gap-2\">\n                Id:\n                <div\n                  title={image.Id}\n                  className=\"max-w-[150px] overflow-hidden text-ellipsis\"\n                >\n                  {image.Id}\n                </div>\n              </div>\n            </>\n          ) : null}\n        </div>\n      </div>\n\n      {/* MAYBE DELETE */}\n      {canExecute && unused && (\n        <ConfirmButton\n          variant=\"destructive\"\n          title=\"Delete Image\"\n          icon={<Trash className=\"w-4 h-4\" />}\n          loading={deletePending}\n          onClick={() => deleteImage({ server: id, name: image_name })}\n        />\n      )}\n\n      {containers && containers.length > 0 && (\n        <DockerContainersSection server_id={id} containers={containers} />\n      )}\n\n      {/* TOP LEVEL IMAGE INFO */}\n      <Section title=\"Details\" icon={<Info className=\"w-4 h-4\" />}>\n        <DataTable\n          tableKey=\"image-info\"\n          data={[image]}\n          columns={[\n            {\n              accessorKey: \"Architecture\",\n              header: \"Architecture\",\n            },\n            {\n              accessorKey: \"Os\",\n              header: \"Os\",\n            },\n            {\n              accessorKey: \"Size\",\n              header: \"Size\",\n              cell: ({ row }) =>\n                row.original.Size\n                  ? format_size_bytes(row.original.Size)\n                  : \"Unknown\",\n            },\n          ]}\n        />\n      </Section>\n\n      {history && history.length > 0 && (\n        <Section title=\"History\" icon={<HistoryIcon className=\"w-4 h-4\" />}>\n          <DataTable\n            tableKey=\"image-history\"\n            data={history.toReversed()}\n            columns={[\n              {\n                accessorKey: \"CreatedBy\",\n                header: \"Created By\",\n                size: 400,\n              },\n              {\n                accessorKey: \"Created\",\n                header: \"Timestamp\",\n                cell: ({ row }) =>\n                  fmt_date_with_minutes(new Date(row.original.Created * 1000)),\n                size: 200,\n              },\n            ]}\n          />\n        </Section>\n      )}\n\n      <DockerLabelsSection labels={image?.Config?.Labels} />\n\n      {specific.includes(Types.SpecificPermission.Inspect) && (\n        <Section\n          title=\"Inspect\"\n          icon={<SearchCode className=\"w-4 h-4\" />}\n          titleRight={\n            <div className=\"pl-2\">\n              <ShowHideButton show={showInspect} setShow={setShowInspect} />\n            </div>\n          }\n        >\n          {showInspect && (\n            <MonacoEditor\n              value={JSON.stringify(image, null, 2)}\n              language=\"json\"\n              readOnly\n            />\n          )}\n        </Section>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/server-info/network.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { ResourceLink } from \"@components/resources/common\";\nimport { useServer } from \"@components/resources/server\";\nimport {\n  ConfirmButton,\n  DOCKER_LINK_ICONS,\n  DockerLabelsSection,\n  DockerOptions,\n  DockerResourceLink,\n  DockerResourcePageName,\n  ShowHideButton,\n} from \"@components/util\";\nimport { useExecute, usePermissions, useRead, useSetTitle } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Badge } from \"@ui/badge\";\nimport { Button } from \"@ui/button\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport {\n  Box,\n  ChevronLeft,\n  Info,\n  Loader2,\n  SearchCode,\n  Trash,\n  Waypoints,\n} from \"lucide-react\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport { useState } from \"react\";\nimport { MonacoEditor } from \"@components/monaco\";\n\nexport default function NetworkPage() {\n  const { type, id, network } = useParams() as {\n    type: string;\n    id: string;\n    network: string;\n  };\n  if (type !== \"servers\") {\n    return <div>This resource type does not have any networks.</div>;\n  }\n  return <NetworkPageInner id={id} network={decodeURIComponent(network)} />;\n}\n\nconst NetworkPageInner = ({\n  id,\n  network: network_name,\n}: {\n  id: string;\n  network: string;\n}) => {\n  const [showInspect, setShowInspect] = useState(false);\n  const server = useServer(id);\n  useSetTitle(`${server?.name} | network | ${network_name}`);\n  const nav = useNavigate();\n\n  const { canExecute, specific } = usePermissions({ type: \"Server\", id });\n\n  const {\n    data: network,\n    isPending,\n    isError,\n  } = useRead(\"InspectDockerNetwork\", {\n    server: id,\n    network: network_name,\n  });\n\n  const { mutate: deleteNetwork, isPending: deletePending } = useExecute(\n    \"DeleteNetwork\",\n    {\n      onSuccess: () => nav(\"/servers/\" + id),\n    }\n  );\n\n  if (isPending) {\n    return (\n      <div className=\"flex justify-center w-full py-4\">\n        <Loader2 className=\"w-8 h-8 animate-spin\" />\n      </div>\n    );\n  }\n\n  if (isError) {\n    return <div className=\"flex w-full py-4\">Failed to inspect network.</div>;\n  }\n\n  if (!network) {\n    return (\n      <div className=\"flex w-full py-4\">\n        No network found with given name: {network_name}\n      </div>\n    );\n  }\n\n  const containers = Object.values(network.Containers ?? {});\n\n  const ipam_driver = network.IPAM?.Driver;\n  const ipam_config =\n    network.IPAM?.Config.map((config) => ({\n      ...config,\n      Driver: ipam_driver,\n    })) ?? [];\n\n  const unused =\n    ![\"none\", \"host\", \"bridge\"].includes(network_name) &&\n    containers &&\n    containers.length === 0\n      ? true\n      : false;\n\n  return (\n    <div className=\"flex flex-col gap-16 mb-24\">\n      {/* HEADER */}\n      <div className=\"flex flex-col gap-4\">\n        {/* BACK */}\n        <div className=\"flex items-center justify-between mb-4\">\n          <Button\n            className=\"gap-2\"\n            variant=\"secondary\"\n            onClick={() => nav(\"/servers/\" + id)}\n          >\n            <ChevronLeft className=\"w-4\" /> Back\n          </Button>\n        </div>\n\n        {/* TITLE */}\n        <div className=\"flex items-center gap-4\">\n          <div className=\"mt-1\">\n            <DOCKER_LINK_ICONS.network\n              server_id={id}\n              name={network_name}\n              size={8}\n            />\n          </div>\n          <DockerResourcePageName name={network_name} />\n          {unused && <Badge variant=\"destructive\">Unused</Badge>}\n        </div>\n\n        {/* INFO */}\n        <div className=\"flex flex-wrap gap-4 items-center text-muted-foreground\">\n          <ResourceLink type=\"Server\" id={id} />|\n          <div className=\"flex gap-2\">\n            <span>IPV6:</span>\n            <span>{network.EnableIPv6 ? \"Enabled\" : \"Disabled\"}</span>\n          </div>\n          {network.Id ? (\n            <>\n              |\n              <div className=\"flex gap-2\">\n                Id:\n                <div\n                  title={network.Id}\n                  className=\"max-w-[150px] overflow-hidden text-ellipsis\"\n                >\n                  {network.Id}\n                </div>\n              </div>\n            </>\n          ) : null}\n        </div>\n      </div>\n\n      {/* MAYBE DELETE */}\n      {canExecute && unused && (\n        <ConfirmButton\n          variant=\"destructive\"\n          title=\"Delete Network\"\n          icon={<Trash className=\"w-4 h-4\" />}\n          loading={deletePending}\n          onClick={() => deleteNetwork({ server: id, name: network_name })}\n        />\n      )}\n\n      {containers.length > 0 && (\n        <Section title=\"Containers\" icon={<Box className=\"w-4 h-4\" />}>\n          <DataTable\n            tableKey=\"network-containers\"\n            data={containers}\n            columns={[\n              {\n                accessorKey: \"Name\",\n                header: ({ column }) => (\n                  <SortableHeader column={column} title=\"Name\" />\n                ),\n                cell: ({ row }) =>\n                  row.original.Name ? (\n                    <DockerResourceLink\n                      type=\"container\"\n                      server_id={id}\n                      name={row.original.Name}\n                    />\n                  ) : (\n                    \"Unknown\"\n                  ),\n                size: 200,\n              },\n              {\n                accessorKey: \"IPv4Address\",\n                header: ({ column }) => (\n                  <SortableHeader column={column} title=\"IPv4\" />\n                ),\n                cell: ({ row }) => row.original.IPv4Address || \"None\",\n              },\n              {\n                accessorKey: \"IPv6Address\",\n                header: ({ column }) => (\n                  <SortableHeader column={column} title=\"IPv6\" />\n                ),\n                cell: ({ row }) => row.original.IPv6Address || \"None\",\n              },\n              {\n                accessorKey: \"MacAddress\",\n                header: ({ column }) => (\n                  <SortableHeader column={column} title=\"Mac\" />\n                ),\n                cell: ({ row }) => row.original.MacAddress || \"None\",\n              },\n            ]}\n          />\n        </Section>\n      )}\n\n      {/* TOP LEVEL NETWORK INFO */}\n      <Section title=\"Details\" icon={<Info className=\"w-4 h-4\" />}>\n        <DataTable\n          tableKey=\"network-info\"\n          data={[network]}\n          columns={[\n            {\n              accessorKey: \"Driver\",\n              header: \"Driver\",\n            },\n            {\n              accessorKey: \"Scope\",\n              header: \"Scope\",\n            },\n            {\n              accessorKey: \"Attachable\",\n              header: \"Attachable\",\n            },\n            {\n              accessorKey: \"Internal\",\n              header: \"Internal\",\n            },\n          ]}\n        />\n        <DockerOptions options={network.Options} />\n      </Section>\n\n      {ipam_config.length > 0 && (\n        <Section title=\"IPAM\" icon={<Waypoints className=\"w-4 h-4\" />}>\n          <DataTable\n            tableKey=\"network-ipam\"\n            data={ipam_config}\n            columns={[\n              {\n                accessorKey: \"Driver\",\n                header: \"Driver\",\n              },\n              {\n                accessorKey: \"Subnet\",\n                header: \"Subnet\",\n              },\n              {\n                accessorKey: \"Gateway\",\n                header: \"Gateway\",\n              },\n              {\n                accessorKey: \"IPRange\",\n                header: \"IPRange\",\n              },\n            ]}\n          />\n          <DockerOptions options={network.IPAM?.Options} />\n        </Section>\n      )}\n\n      <DockerLabelsSection labels={network.Labels} />\n\n      {specific.includes(Types.SpecificPermission.Inspect) && (\n        <Section\n          title=\"Inspect\"\n          icon={<SearchCode className=\"w-4 h-4\" />}\n          titleRight={\n            <div className=\"pl-2\">\n              <ShowHideButton show={showInspect} setShow={setShowInspect} />\n            </div>\n          }\n        >\n          {showInspect && (\n            <MonacoEditor\n              value={JSON.stringify(network, null, 2)}\n              language=\"json\"\n              readOnly\n            />\n          )}\n        </Section>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/server-info/volume.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport { ResourceLink } from \"@components/resources/common\";\nimport { useServer } from \"@components/resources/server\";\nimport {\n  ConfirmButton,\n  DOCKER_LINK_ICONS,\n  DockerContainersSection,\n  DockerLabelsSection,\n  DockerOptions,\n  DockerResourcePageName,\n  ShowHideButton,\n} from \"@components/util\";\nimport { useExecute, usePermissions, useRead, useSetTitle } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Badge } from \"@ui/badge\";\nimport { Button } from \"@ui/button\";\nimport { DataTable } from \"@ui/data-table\";\nimport { ChevronLeft, Info, Loader2, SearchCode, Trash } from \"lucide-react\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport { useState } from \"react\";\nimport { MonacoEditor } from \"@components/monaco\";\n\nexport default function VolumePage() {\n  const { type, id, volume } = useParams() as {\n    type: string;\n    id: string;\n    volume: string;\n  };\n  if (type !== \"servers\") {\n    return <div>This resource type does not have any volumes.</div>;\n  }\n  return <VolumePageInner id={id} volume={decodeURIComponent(volume)} />;\n}\n\nconst VolumePageInner = ({\n  id,\n  volume: volume_name,\n}: {\n  id: string;\n  volume: string;\n}) => {\n  const [showInspect, setShowInspect] = useState(false);\n  const server = useServer(id);\n  useSetTitle(`${server?.name} | volume | ${volume_name}`);\n  const nav = useNavigate();\n\n  const { canExecute, specific } = usePermissions({ type: \"Server\", id });\n\n  const {\n    data: volume,\n    isPending,\n    isError,\n  } = useRead(\"InspectDockerVolume\", {\n    server: id,\n    volume: volume_name,\n  });\n\n  const containers = useRead(\n    \"ListDockerContainers\",\n    {\n      server: id,\n    },\n    { refetchInterval: 10_000 }\n  ).data?.filter((container) => container.volumes?.includes(volume_name));\n\n  const { mutate: deleteVolume, isPending: deletePending } = useExecute(\n    \"DeleteVolume\",\n    {\n      onSuccess: () => nav(\"/servers/\" + id),\n    }\n  );\n\n  if (isPending) {\n    return (\n      <div className=\"flex justify-center w-full py-4\">\n        <Loader2 className=\"w-8 h-8 animate-spin\" />\n      </div>\n    );\n  }\n\n  if (isError) {\n    return <div className=\"flex w-full py-4\">Failed to inspect volume.</div>;\n  }\n\n  if (!volume) {\n    return (\n      <div className=\"flex w-full py-4\">\n        No volume found with given name: {volume_name}\n      </div>\n    );\n  }\n\n  const unused = containers && containers.length === 0 ? true : false;\n\n  return (\n    <div className=\"flex flex-col gap-16 mb-24\">\n      {/* HEADER */}\n      <div className=\"flex flex-col gap-4\">\n        {/* BACK */}\n        <div className=\"flex items-center justify-between mb-4\">\n          <Button\n            className=\"gap-2\"\n            variant=\"secondary\"\n            onClick={() => nav(\"/servers/\" + id)}\n          >\n            <ChevronLeft className=\"w-4\" /> Back\n          </Button>\n        </div>\n\n        {/* TITLE */}\n        <div className=\"flex items-center gap-4\">\n          <div className=\"mt-1\">\n            <DOCKER_LINK_ICONS.volume\n              server_id={id}\n              name={volume_name}\n              size={8}\n            />\n          </div>\n          <DockerResourcePageName name={volume_name} />\n          {containers && containers.length === 0 && (\n            <Badge variant=\"destructive\">Unused</Badge>\n          )}\n        </div>\n\n        {/* INFO */}\n        <div className=\"flex flex-wrap gap-4 items-center text-muted-foreground\">\n          <ResourceLink type=\"Server\" id={id} />\n        </div>\n      </div>\n\n      {/* MAYBE DELETE */}\n      {canExecute && unused && (\n        <ConfirmButton\n          variant=\"destructive\"\n          title=\"Delete Volume\"\n          icon={<Trash className=\"w-4 h-4\" />}\n          loading={deletePending}\n          onClick={() => deleteVolume({ server: id, name: volume_name })}\n        />\n      )}\n\n      {containers && containers.length > 0 && (\n        <DockerContainersSection server_id={id} containers={containers} />\n      )}\n\n      {/* TOP LEVEL VOLUME INFO */}\n      <Section title=\"Details\" icon={<Info className=\"w-4 h-4\" />}>\n        <DataTable\n          tableKey=\"volume-info\"\n          data={[volume]}\n          columns={[\n            {\n              accessorKey: \"Driver\",\n              header: \"Driver\",\n            },\n            {\n              accessorKey: \"Scope\",\n              header: \"Scope\",\n            },\n            {\n              accessorKey: \"CreatedAt\",\n              header: \"Created At\",\n            },\n            {\n              accessorKey: \"UsageData.Size\",\n              header: \"Used Size\",\n            },\n          ]}\n        />\n        <DockerOptions options={volume.Options} />\n      </Section>\n\n      <DockerLabelsSection labels={volume.Labels} />\n\n      {specific.includes(Types.SpecificPermission.Inspect) && (\n        <Section\n          title=\"Inspect\"\n          icon={<SearchCode className=\"w-4 h-4\" />}\n          titleRight={\n            <div className=\"pl-2\">\n              <ShowHideButton show={showInspect} setShow={setShowInspect} />\n            </div>\n          }\n        >\n          {showInspect && (\n            <MonacoEditor\n              value={JSON.stringify(volume, null, 2)}\n              language=\"json\"\n              readOnly\n            />\n          )}\n        </Section>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/settings/index.tsx",
    "content": "import { lazy } from \"react\";\nimport { useSettingsView, useUser } from \"@lib/hooks\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@ui/tabs\";\nimport { Page } from \"@components/layouts\";\nimport { ExportButton } from \"@components/export\";\nimport { Variables } from \"./variables\";\nimport { Tags } from \"./tags\";\nimport { UsersPage } from \"./users\";\nimport { Profile } from \"./profile\";\nimport { ProvidersPage } from \"./providers\";\n\nconst Resources = lazy(() => import(\"@pages/resources\"));\n\nexport default function Settings() {\n  const user = useUser().data;\n  const [view, setView] = useSettingsView();\n  const currentView =\n    (view === \"Users\" || view === \"Providers\") && !user?.admin\n      ? \"Variables\"\n      : view;\n  return (\n    <Page>\n      <Tabs\n        value={currentView}\n        onValueChange={setView as any}\n        className=\"flex flex-col gap-6\"\n      >\n        <div className=\"flex items-center justify-between\">\n          <TabsList className=\"justify-start w-fit\">\n            <TabsTrigger value=\"Variables\">Variables</TabsTrigger>\n            <TabsTrigger value=\"Tags\">Tags</TabsTrigger>\n            <TabsTrigger value=\"Builders\">Builders</TabsTrigger>\n            <TabsTrigger value=\"Alerters\">Alerters</TabsTrigger>\n            {user?.admin && (\n              <TabsTrigger value=\"Providers\">Providers</TabsTrigger>\n            )}\n            {user?.admin && <TabsTrigger value=\"Users\">Users</TabsTrigger>}\n            <TabsTrigger value=\"Profile\">Profile</TabsTrigger>\n          </TabsList>\n\n          {currentView === \"Variables\" && <ExportButton include_variables />}\n        </div>\n\n        <TabsContent value=\"Variables\">\n          <Variables />\n        </TabsContent>\n        <TabsContent value=\"Tags\">\n          <Tags />\n        </TabsContent>\n        <TabsContent value=\"Builders\">\n          <Resources _type=\"Builder\" />\n        </TabsContent>\n        <TabsContent value=\"Alerters\">\n          <Resources _type=\"Alerter\" />\n        </TabsContent>\n        {user?.admin && (\n          <TabsContent value=\"Providers\">\n            <ProvidersPage />\n          </TabsContent>\n        )}\n        {user?.admin && (\n          <TabsContent value=\"Users\">\n            <UsersPage goToProfile={() => setView(\"Profile\")} />\n          </TabsContent>\n        )}\n        <TabsContent value=\"Profile\">\n          <Profile />\n        </TabsContent>\n      </Tabs>\n    </Page>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/settings/profile.tsx",
    "content": "import { ConfirmButton, CopyButton } from \"@components/util\";\nimport {\n  useInvalidate,\n  useManageUser,\n  useRead,\n  useSetTitle,\n  useUser,\n  useWrite,\n} from \"@lib/hooks\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { Button } from \"@ui/button\";\nimport { useToast } from \"@ui/use-toast\";\nimport {\n  Trash,\n  PlusCircle,\n  Loader2,\n  Check,\n  User,\n  Eye,\n  EyeOff,\n  KeyRound,\n  UserPen,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Input } from \"@ui/input\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@ui/dropdown-menu\";\nimport { KeysTable } from \"@components/keys/table\";\nimport { Section } from \"@components/layouts\";\nimport { Card, CardHeader } from \"@ui/card\";\nimport { Types } from \"komodo_client\";\n\nexport const Profile = () => {\n  useSetTitle(\"Profile\");\n  const user = useUser().data;\n  if (!user) {\n    return (\n      <div className=\"w-full h-[400px] flex justify-center items-center\">\n        <Loader2 className=\"w-8 h-8 animate-spin\" />\n      </div>\n    );\n  }\n  return <ProfileInner user={user} />;\n};\n\nconst ProfileInner = ({ user }: { user: Types.User }) => {\n  const { refetch: refetchUser } = useUser();\n  const { toast } = useToast();\n  const keys = useRead(\"ListApiKeys\", {}).data ?? [];\n  const [username, setUsername] = useState(user.username);\n  const [password, setPassword] = useState(\"\");\n  const [hidePassword, setHidePassword] = useState(true);\n  const { mutate: updateUsername } = useWrite(\"UpdateUserUsername\", {\n    onSuccess: () => {\n      toast({ title: \"Username updated.\" });\n      refetchUser();\n    },\n  });\n  const { mutate: updatePassword } = useWrite(\"UpdateUserPassword\", {\n    onSuccess: () => {\n      toast({ title: \"Password updated.\" });\n      setPassword(\"\");\n    },\n  });\n  return (\n    <div className=\"flex flex-col gap-6\">\n      {/* Profile */}\n      <Section title=\"Profile\" icon={<User className=\"w-4 h-4\" />}>\n        <Card>\n          <CardHeader className=\"gap-4\">\n            {/* Profile Info */}\n            <UserProfile user={user} />\n\n            {/* Update Username */}\n            <div className=\"flex items-center gap-4\">\n              <div className=\"text-muted-foreground font-mono\">Username:</div>\n              <div className=\"w-[200px] lg:w-[300px]\">\n                <Input\n                  placeholder=\"Input username\"\n                  value={username}\n                  onChange={(e) => setUsername(e.target.value)}\n                />\n              </div>\n              <ConfirmButton\n                title=\"Update Username\"\n                icon={<UserPen className=\"w-4 h-4\" />}\n                onClick={() => updateUsername({ username })}\n                disabled={!username || username === user.username}\n              />\n            </div>\n\n            {/* Update Password */}\n            {user.config.type === \"Local\" && (\n              <div className=\"flex items-center gap-4\">\n                <div className=\"text-muted-foreground font-mono\">Password:</div>\n                <div className=\"w-[200px] lg:w-[300px] flex items-center gap-2\">\n                  <Input\n                    placeholder=\"Input password\"\n                    type={hidePassword ? \"password\" : \"text\"}\n                    value={password}\n                    onChange={(e) => setPassword(e.target.value)}\n                  />\n                  <Button\n                    size=\"icon\"\n                    variant=\"outline\"\n                    onClick={() => setHidePassword((curr) => !curr)}\n                  >\n                    {hidePassword ? (\n                      <EyeOff className=\"w-4 h-4\" />\n                    ) : (\n                      <Eye className=\"w-4 h-4\" />\n                    )}\n                  </Button>\n                </div>\n                <ConfirmButton\n                  title=\"Update Password\"\n                  icon={<UserPen className=\"w-4 h-4\" />}\n                  onClick={() => updatePassword({ password })}\n                  disabled={!password}\n                />\n              </div>\n            )}\n          </CardHeader>\n        </Card>\n      </Section>\n\n      {/* Api Keys */}\n      <Section title=\"Api Keys\" icon={<KeyRound className=\"w-4 h-4\" />}>\n        <div>\n          <CreateKey />\n        </div>\n        <KeysTable keys={keys} DeleteKey={DeleteKey} />\n      </Section>\n    </div>\n  );\n};\n\nconst UserProfile = ({ user }: { user: Types.User }) => {\n  return (\n    <div className=\"flex items-center gap-4 flex-wrap\">\n      <div className=\"font-mono text-muted-foreground\">Type:</div>\n      {user.config.type}\n\n      <div className=\"font-mono text-muted-foreground\">|</div>\n\n      <div className=\"font-mono text-muted-foreground\">Admin:</div>\n      {user.admin ? \"True\" : \"False\"}\n\n      {user.admin && (\n        <>\n          <div className=\"font-mono text-muted-foreground\">|</div>\n\n          <div className=\"font-mono text-muted-foreground\">Super Admin:</div>\n          {user.super_admin ? \"True\" : \"False\"}\n        </>\n      )}\n    </div>\n  );\n};\n\nconst ONE_DAY_MS = 1000 * 60 * 60 * 24;\n\ntype ExpiresOptions = \"90 days\" | \"180 days\" | \"1 year\" | \"never\";\n\nconst CreateKey = () => {\n  const [open, setOpen] = useState(false);\n  const [name, setName] = useState(\"\");\n  const [expires, setExpires] = useState<ExpiresOptions>(\"never\");\n  const [submitted, setSubmitted] = useState<{ key: string; secret: string }>();\n  const invalidate = useInvalidate();\n  const { mutate, isPending } = useManageUser(\"CreateApiKey\", {\n    onSuccess: ({ key, secret }) => {\n      invalidate([\"ListApiKeys\"]);\n      setSubmitted({ key, secret });\n    },\n  });\n  const now = Date.now();\n  const expiresOptions: Record<ExpiresOptions, number> = {\n    \"90 days\": now + ONE_DAY_MS * 90,\n    \"180 days\": now + ONE_DAY_MS * 180,\n    \"1 year\": now + ONE_DAY_MS * 365,\n    never: 0,\n  };\n  const submit = () => mutate({ name, expires: expiresOptions[expires] });\n  const onOpenChange = (open: boolean) => {\n    setOpen(open);\n    if (!open) {\n      setName(\"\");\n      setExpires(\"never\");\n      setSubmitted(undefined);\n    }\n  };\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogTrigger asChild>\n        <Button variant=\"secondary\" className=\"items-center gap-2\">\n          New Api Key <PlusCircle className=\"w-4 h-4\" />\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        {submitted ? (\n          <>\n            <DialogHeader>\n              <DialogTitle>Api Key Created</DialogTitle>\n            </DialogHeader>\n            <div className=\"py-8 flex flex-col gap-4\">\n              <div className=\"flex items-center justify-between\">\n                Key\n                <Input className=\"w-72\" value={submitted.key} disabled />\n                <CopyButton content={submitted.key} />\n              </div>\n              <div className=\"flex items-center justify-between\">\n                Secret\n                <Input className=\"w-72\" value={submitted.secret} disabled />\n                <CopyButton content={submitted.secret} />\n              </div>\n            </div>\n            <DialogFooter className=\"flex justify-end\">\n              <Button\n                variant=\"secondary\"\n                className=\"gap-4\"\n                onClick={() => onOpenChange(false)}\n              >\n                Confirm <Check className=\"w-4\" />\n              </Button>\n            </DialogFooter>\n          </>\n        ) : (\n          <>\n            <DialogHeader>\n              <DialogTitle>Create Api Key</DialogTitle>\n            </DialogHeader>\n            <div className=\"py-8 flex flex-col gap-4\">\n              <div className=\"flex items-center justify-between\">\n                Name\n                <Input\n                  className=\"w-72\"\n                  value={name}\n                  onChange={(e) => setName(e.target.value)}\n                />\n              </div>\n              <div className=\"flex items-center justify-between\">\n                Expiry\n                <DropdownMenu>\n                  <DropdownMenuTrigger asChild>\n                    <Button\n                      className=\"w-36 justify-between px-3\"\n                      variant=\"outline\"\n                    >\n                      {expires}\n                    </Button>\n                  </DropdownMenuTrigger>\n                  <DropdownMenuContent className=\"w-36\" side=\"bottom\">\n                    <DropdownMenuGroup>\n                      {Object.keys(expiresOptions)\n                        .filter((option) => option !== expires)\n                        .map((option) => (\n                          <DropdownMenuItem\n                            key={option}\n                            onClick={() => setExpires(option as any)}\n                          >\n                            {option}\n                          </DropdownMenuItem>\n                        ))}\n                    </DropdownMenuGroup>\n                  </DropdownMenuContent>\n                </DropdownMenu>\n              </div>\n            </div>\n            <DialogFooter className=\"flex justify-end\">\n              <Button\n                variant=\"secondary\"\n                className=\"gap-4\"\n                onClick={submit}\n                disabled={isPending}\n              >\n                Submit\n                {isPending ? (\n                  <Loader2 className=\"w-4 animate-spin\" />\n                ) : (\n                  <Check className=\"w-4\" />\n                )}\n              </Button>\n            </DialogFooter>\n          </>\n        )}\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst DeleteKey = ({ api_key }: { api_key: string }) => {\n  const invalidate = useInvalidate();\n  const { toast } = useToast();\n  const { mutate, isPending } = useManageUser(\"DeleteApiKey\", {\n    onSuccess: () => {\n      invalidate([\"ListApiKeys\"]);\n      toast({ title: \"Api Key Deleted\" });\n    },\n    onError: () => {\n      toast({ title: \"Failed to delete api key\", variant: \"destructive\" });\n    },\n  });\n  return (\n    <ConfirmButton\n      title=\"Delete\"\n      variant=\"destructive\"\n      icon={<Trash className=\"w-4 h-4\" />}\n      onClick={(e) => {\n        e.stopPropagation();\n        mutate({ key: api_key });\n      }}\n      loading={isPending}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/settings/providers.tsx",
    "content": "import {\n  ConfirmButton,\n  CopyButton,\n  TextUpdateMenuMonaco,\n} from \"@components/util\";\nimport {\n  useInvalidate,\n  useRead,\n  useSetTitle,\n  useUser,\n  useWrite,\n} from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Button } from \"@ui/button\";\nimport { Card } from \"@ui/card\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { Input } from \"@ui/input\";\nimport { Switch } from \"@ui/switch\";\nimport { useToast } from \"@ui/use-toast\";\nimport {\n  Check,\n  GitBranch,\n  HardDrive,\n  Loader2,\n  PlusCircle,\n  Search,\n  Trash,\n} from \"lucide-react\";\nimport { ChangeEvent, ReactNode, useState } from \"react\";\nimport { Section } from \"@components/layouts\";\nimport { Badge } from \"@ui/badge\";\n\nexport const ProvidersPage = () => {\n  return (\n    <div className=\"flex flex-col gap-6\">\n      <Providers type=\"GitProvider\" />\n      <Providers type=\"DockerRegistry\" />\n    </div>\n  );\n};\n\nconst Providers = ({ type }: { type: \"GitProvider\" | \"DockerRegistry\" }) => {\n  const user = useUser().data;\n  const disabled = !user?.admin;\n  useSetTitle(\"Providers\");\n  const [updateMenuData, setUpdateMenuData] = useState<\n    | false\n    | {\n        title: string;\n        value: string;\n        placeholder: string;\n        onUpdate: (value: string) => void;\n        titleRight?: ReactNode;\n      }\n  >(false);\n  const [search, setSearch] = useState(\"\");\n  const accounts = useRead(`List${type}Accounts`, {}).data ?? [];\n  const searchSplit = search?.toLowerCase().split(\" \") || [];\n  const filtered =\n    accounts?.filter((account) => {\n      if (searchSplit.length > 0) {\n        const domain = account.domain?.toLowerCase();\n        const username = account.username?.toLowerCase();\n        return searchSplit.every(\n          (search) =>\n            domain.includes(search) || (username && username.includes(search))\n        );\n      } else return true;\n    }) ?? [];\n  const { toast } = useToast();\n  const inv = useInvalidate();\n  const { mutate: updateAccount } = useWrite(`Update${type}Account`, {\n    onSuccess: () => {\n      inv([`List${type}Accounts`], [`Get${type}Account`]);\n      toast({ title: \"Updated account\" });\n    },\n  });\n  return (\n    <Section\n      title={type === \"DockerRegistry\" ? \"Registry Accounts\" : \"Git Accounts\"}\n      icon={\n        type === \"DockerRegistry\" ? (\n          <HardDrive className=\"w-4 h-4\" />\n        ) : (\n          <GitBranch className=\"w-4 h-4\" />\n        )\n      }\n    >\n      {/* Create / Search */}\n      <div className=\"flex items-center justify-between\">\n        <CreateAccount type={type} />\n        <div className=\"relative\">\n          <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n          <Input\n            placeholder=\"search...\"\n            value={search}\n            onChange={(e) => setSearch(e.target.value)}\n            className=\"pl-8 w-[200px] lg:w-[300px]\"\n          />\n        </div>\n      </div>\n\n      {/* ACCOUNTS */}\n      <DataTable\n        tableKey={type + \"-accounts\"}\n        data={filtered}\n        columns={[\n          {\n            accessorKey: \"domain\",\n            size: 200,\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Domain\" />\n            ),\n            cell: ({ row }) => {\n              return (\n                <div className=\"flex items-center gap-2\">\n                  <Card\n                    className=\"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer w-full\"\n                    onClick={() => {\n                      setUpdateMenuData({\n                        title: \"Set Domain\",\n                        value: row.original.domain ?? \"\",\n                        placeholder: \"Input domain, eg. git.komo.do\",\n                        titleRight:\n                          type === \"GitProvider\" ? (\n                            <UpdateHttps id={row.original._id?.$oid!} />\n                          ) : undefined,\n                        onUpdate: (domain) => {\n                          if (row.original.domain === domain) {\n                            return;\n                          }\n                          updateAccount({\n                            id: row.original._id?.$oid!,\n                            account: { domain },\n                          });\n                        },\n                      });\n                    }}\n                  >\n                    <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis text-muted-foreground w-[100px] xl:w-[150px] 2xl:w-[200px]\">\n                      {row.original.domain || \"Set domain\"}\n                    </div>\n                  </Card>\n                  <CopyButton content={row.original.domain} />\n                </div>\n              );\n            },\n          },\n          {\n            accessorKey: \"username\",\n            size: 200,\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Username\" />\n            ),\n            cell: ({ row }) => {\n              return (\n                <div className=\"flex items-center gap-2\">\n                  <Card\n                    className=\"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer w-full\"\n                    onClick={() => {\n                      setUpdateMenuData({\n                        title: \"Set Username\",\n                        value: row.original.username ?? \"\",\n                        placeholder: \"Input account username\",\n                        onUpdate: (username) => {\n                          if (row.original.username === username) {\n                            return;\n                          }\n                          updateAccount({\n                            id: row.original._id?.$oid!,\n                            account: { username },\n                          });\n                        },\n                      });\n                    }}\n                  >\n                    <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis text-muted-foreground w-[100px] xl:w-[150px] 2xl:w-[200px]\">\n                      {row.original.username || \"Set username\"}\n                    </div>\n                  </Card>\n                  <CopyButton content={row.original.username} />\n                </div>\n              );\n            },\n          },\n          {\n            accessorKey: \"token\",\n            size: 200,\n            header: ({ column }) => (\n              <SortableHeader column={column} title=\"Token\" />\n            ),\n            cell: ({ row }) => {\n              return (\n                <div className=\"flex items-center gap-2\">\n                  <Card\n                    className=\"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer w-full\"\n                    onClick={() => {\n                      setUpdateMenuData({\n                        title: \"Set Token\",\n                        value: row.original.token ?? \"\",\n                        placeholder: \"Input account token\",\n                        onUpdate: (token) => {\n                          if (row.original.token === token) {\n                            return;\n                          }\n                          updateAccount({\n                            id: row.original._id?.$oid!,\n                            account: { token },\n                          });\n                        },\n                      });\n                    }}\n                  >\n                    <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis text-muted-foreground w-[100px] xl:w-[150px] 2xl:w-[200px]\">\n                      {\"*\".repeat(row.original.token?.length || 0) ||\n                        \"Set token\"}\n                    </div>\n                  </Card>\n                  <CopyButton content={row.original.token} />\n                </div>\n              );\n            },\n          },\n          {\n            header: \"Delete\",\n            maxSize: 200,\n            cell: ({ row }) => (\n              <DeleteAccount type={type} id={row.original._id?.$oid!} />\n            ),\n          },\n        ]}\n      />\n      <ProvidersFromConfig type={type} />\n      {updateMenuData && (\n        <TextUpdateMenuMonaco\n          title={updateMenuData.title}\n          titleRight={updateMenuData.titleRight}\n          placeholder={updateMenuData.placeholder}\n          value={updateMenuData.value}\n          onUpdate={updateMenuData.onUpdate}\n          triggerClassName=\"w-full\"\n          disabled={disabled}\n          open={!!updateMenuData}\n          setOpen={(open) => {\n            if (!open) {\n              setUpdateMenuData(false);\n            }\n          }}\n          triggerHidden\n        />\n      )}\n    </Section>\n  );\n};\n\nconst ProvidersFromConfig = ({\n  type,\n}: {\n  type: \"GitProvider\" | \"DockerRegistry\";\n}) => {\n  const accounts = useRead(\n    type === \"GitProvider\"\n      ? \"ListGitProvidersFromConfig\"\n      : \"ListDockerRegistriesFromConfig\",\n    {}\n  )\n    .data?.map((provider) =>\n      provider.accounts.map((account) => [provider.domain, account.username])\n    )\n    .flat(1);\n  if (!accounts)\n    return (\n      <div className=\"w-full flex justify-center\">\n        <Loader2 className=\"w-4 h-4 animate-spin\" />\n      </div>\n    );\n  if (accounts.length === 0) return;\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <div className=\"text-muted-foreground\">From config file:</div>\n      <div className=\"flex gap-3 flex-wrap\">\n        {accounts.map(([domain, username]) => (\n          <Badge variant=\"secondary\">\n            {domain} - {username}\n          </Badge>\n        ))}\n      </div>\n    </div>\n  );\n};\n\nconst UpdateHttps = ({ id }: { id: string }) => {\n  const account = useRead(\"ListGitProviderAccounts\", {}).data?.find(\n    (account) => account._id?.$oid === id\n  ) as Types.GitProviderAccount;\n  const { toast } = useToast();\n  const inv = useInvalidate();\n  const { mutate: updateAccount } = useWrite(\"UpdateGitProviderAccount\", {\n    onSuccess: () => {\n      inv([\"ListGitProviderAccounts\"], [\"GetGitProviderAccount\", { id }]);\n      toast({ title: \"Updated account\" });\n    },\n  });\n  return (\n    <div className=\"flex items-center gap-2\">\n      <div>Https:</div>\n      <Switch\n        checked={account.https}\n        onCheckedChange={(https) =>\n          updateAccount({\n            id,\n            account: { https },\n          })\n        }\n      />\n    </div>\n  );\n};\n\nconst CreateAccount = ({\n  type,\n}: {\n  type: \"GitProvider\" | \"DockerRegistry\";\n}) => {\n  const { toast } = useToast();\n  const [open, setOpen] = useState(false);\n  const [domain, setDomain] = useState(\"\");\n  const [https, setHttps] = useState(true);\n  const [username, setUsername] = useState(\"\");\n  const [token, setToken] = useState(\"\");\n  const invalidate = useInvalidate();\n  const { mutate: create, isPending } = useWrite(`Create${type}Account`, {\n    onSuccess: () => {\n      invalidate([`List${type}Accounts`]);\n      toast({ title: \"Account created\" });\n      setOpen(false);\n    },\n  });\n  const submit = () => create({ account: { domain, https, username, token } });\n  const form: Array<\n    | undefined\n    | [string, string, (e: ChangeEvent<HTMLInputElement>) => void, false]\n    | [string, boolean, (checked: boolean) => void, true]\n  > = [\n    [\n      \"Domain\",\n      domain,\n      (e: ChangeEvent<HTMLInputElement>) => setDomain(e.target.value),\n      false,\n    ],\n    type === \"GitProvider\"\n      ? [\"Use https\", https, (https: boolean) => setHttps(https), true]\n      : undefined,\n    [\n      \"Username\",\n      username,\n      (e: ChangeEvent<HTMLInputElement>) => setUsername(e.target.value),\n      false,\n    ],\n    [\n      \"Token\",\n      token,\n      (e: ChangeEvent<HTMLInputElement>) => setToken(e.target.value),\n      false,\n    ],\n  ];\n  const account_type =\n    type === \"DockerRegistry\" ? \"Registry Account\" : \"Git Account\";\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button variant=\"secondary\" className=\"items-center gap-2\">\n          New Account <PlusCircle className=\"w-4 h-4\" />\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Create {account_type}</DialogTitle>\n        </DialogHeader>\n        <div className=\"py-8 flex flex-col gap-4\">\n          {form.map((item) => {\n            if (!item) return;\n            const [title, value, onChange, bool] = item;\n            if (bool) {\n              return (\n                <div key={title} className=\"flex items-center justify-between\">\n                  {title}\n                  <Switch\n                    checked={value}\n                    onCheckedChange={(checked) => onChange(checked)}\n                  />\n                </div>\n              );\n            }\n            return (\n              <div key={title} className=\"flex items-center justify-between\">\n                {title}\n                <Input\n                  placeholder={`Input ${title.toLowerCase()}`}\n                  className=\"w-72\"\n                  value={value}\n                  onChange={onChange}\n                />\n              </div>\n            );\n          })}\n        </div>\n        <DialogFooter className=\"flex justify-end\">\n          <Button className=\"gap-4\" onClick={submit} disabled={isPending}>\n            Create\n            {isPending ? (\n              <Loader2 className=\"w-4 animate-spin\" />\n            ) : (\n              <Check className=\"w-4\" />\n            )}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst DeleteAccount = ({\n  type,\n  id,\n}: {\n  type: \"GitProvider\" | \"DockerRegistry\";\n  id: string;\n}) => {\n  const invalidate = useInvalidate();\n  const { toast } = useToast();\n  const { mutate, isPending } = useWrite(`Delete${type}Account`, {\n    onSuccess: () => {\n      invalidate([`List${type}Accounts`], [`Get${type}Account`]);\n      toast({ title: \"Account deleted\" });\n    },\n  });\n  return (\n    <ConfirmButton\n      title=\"Delete\"\n      icon={<Trash className=\"w-4 h-4\" />}\n      onClick={() => mutate({ id })}\n      loading={isPending}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/settings/tags.tsx",
    "content": "import { ConfirmButton } from \"@components/util\";\nimport {\n  useInvalidate,\n  useRead,\n  useSetTitle,\n  useUser,\n  useWrite,\n} from \"@lib/hooks\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { Button } from \"@ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@ui/card\";\nimport { useToast } from \"@ui/use-toast\";\nimport {\n  Trash,\n  PlusCircle,\n  Loader2,\n  Check,\n  Search,\n  SearchX,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Input } from \"@ui/input\";\nimport { UpdateUser } from \"@components/updates/details\";\nimport { DataTable } from \"@ui/data-table\";\nimport { Types } from \"komodo_client\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport { cn, filterBySplit } from \"@lib/utils\";\nimport { fmt_upper_camelcase } from \"@lib/formatting\";\nimport { tag_background_class } from \"@lib/color\";\n\nexport const Tags = () => {\n  useSetTitle(\"Tags\");\n  const user = useUser().data!;\n\n  const [search, setSearch] = useState(\"\");\n\n  const tags = useRead(\"ListTags\", {}).data;\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex items-center justify-between\">\n        <CreateTag />\n        <div className=\"relative\">\n          <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n          <Input\n            placeholder=\"search...\"\n            value={search}\n            onChange={(e) => setSearch(e.target.value)}\n            className=\"pl-8 w-[200px] lg:w-[300px]\"\n          />\n        </div>\n      </div>\n      <DataTable\n        tableKey=\"tags\"\n        data={tags?.filter((tag) => tag.name.includes(search)) ?? []}\n        columns={[\n          {\n            header: \"Name\",\n            size: 200,\n            accessorKey: \"name\",\n          },\n          {\n            header: \"Color\",\n            size: 200,\n            cell: ({ row }) => (\n              <ColorSelector\n                tag_id={row.original._id?.$oid!}\n                color={row.original.color!}\n                disabled={!user.admin && row.original.owner !== user._id?.$oid}\n              />\n            ),\n          },\n          {\n            header: \"Owner\",\n            size: 200,\n            cell: ({ row }) =>\n              row.original.owner ? (\n                <UpdateUser user_id={row.original.owner} />\n              ) : (\n                \"Unknown\"\n              ),\n          },\n          {\n            header: \"Delete\",\n            size: 200,\n            cell: ({ row }) => (\n              <DeleteTag\n                tag_id={row.original._id!.$oid}\n                disabled={!user.admin && row.original.owner !== user._id?.$oid}\n              />\n            ),\n          },\n        ]}\n      />\n    </div>\n  );\n};\n\nexport const TagCards = () => {\n  const tags = useRead(\"ListTags\", {}).data;\n  const user = useUser().data!;\n  return (\n    <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n      {tags?.map((tag) => (\n        <Card\n          id={tag._id!.$oid}\n          className=\"h-full hover:bg-accent/50 group-focus:bg-accent/50 transition-colors\"\n        >\n          <CardHeader className=\"flex-row justify-between items-center\">\n            <CardTitle>{tag.name}</CardTitle>\n            <DeleteTag\n              tag_id={tag._id!.$oid}\n              disabled={!user.admin && tag.owner !== user._id?.$oid}\n            />\n          </CardHeader>\n          <CardContent className=\"text-sm text-muted-foreground\">\n            {tag.owner && (\n              <div>\n                owner: <UpdateUser user_id={tag.owner} />\n              </div>\n            )}\n          </CardContent>\n        </Card>\n      ))}\n    </div>\n  );\n};\n\nexport const CreateTag = () => {\n  const { toast } = useToast();\n  const [open, setOpen] = useState(false);\n  const [name, setName] = useState(\"\");\n  const invalidate = useInvalidate();\n  const { mutate, isPending } = useWrite(\"CreateTag\", {\n    onSuccess: () => {\n      invalidate([\"ListTags\"]);\n      toast({ title: \"Tag Created\" });\n      setOpen(false);\n    },\n  });\n  const submit = () => mutate({ name });\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button variant=\"secondary\" className=\"items-center gap-2\">\n          New Tag <PlusCircle className=\"w-4 h-4\" />\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Create Tag</DialogTitle>\n        </DialogHeader>\n        <div className=\"py-8 flex flex-col gap-4\">\n          <div className=\"flex items-center justify-between\">\n            Name\n            <Input\n              className=\"w-72\"\n              value={name}\n              onChange={(e) => setName(e.target.value)}\n            />\n          </div>\n        </div>\n        <DialogFooter className=\"flex justify-end\">\n          <Button className=\"gap-4\" onClick={submit} disabled={isPending}>\n            Submit\n            {isPending ? (\n              <Loader2 className=\"w-4 animate-spin\" />\n            ) : (\n              <Check className=\"w-4\" />\n            )}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst DeleteTag = ({\n  tag_id,\n  disabled,\n}: {\n  tag_id: string;\n  disabled: boolean;\n}) => {\n  const invalidate = useInvalidate();\n  const { toast } = useToast();\n  const { mutate, isPending } = useWrite(\"DeleteTag\", {\n    onSuccess: () => {\n      invalidate([\"ListTags\"]);\n      toast({ title: \"Tag Deleted\" });\n    },\n  });\n  return (\n    <ConfirmButton\n      title=\"Delete\"\n      icon={<Trash className=\"w-4 h-4\" />}\n      onClick={() => mutate({ id: tag_id })}\n      disabled={disabled}\n      loading={isPending}\n    />\n  );\n};\n\nconst ColorSelector = ({\n  tag_id,\n  color,\n  disabled,\n}: {\n  tag_id: string;\n  color: Types.TagColor;\n  disabled: boolean;\n}) => {\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const [loadingColor, setLoadingColor] = useState<Types.TagColor>();\n  const { mutateAsync } = useWrite(\"UpdateTagColor\");\n  const inv = useInvalidate();\n  const onSelect = async (color: Types.TagColor) => {\n    setLoadingColor(color);\n    await mutateAsync({ tag: tag_id, color });\n    inv([\"ListTags\"]);\n    setLoadingColor(undefined);\n    setOpen(false);\n  };\n  const filtered = filterBySplit(\n    Object.values(Types.TagColor),\n    search,\n    (item) => item\n  );\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"secondary\"\n          className=\"flex justify-between gap-2 w-[160px]\"\n          disabled={disabled}\n        >\n          {fmt_upper_camelcase(color) || \"Select Color\"}\n          <div\n            className={cn(\n              \"w-[25px] h-[25px] rounded-sm bg-opacity-70\",\n              tag_background_class(color)\n            )}\n          />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[200px] max-h-[300px] p-0\" align=\"end\">\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Search Colors\"\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center pt-3 pb-2\">\n              {\"No Colors Found\"}\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n\n            <CommandGroup>\n              {filtered.map((color) => (\n                <CommandItem\n                  key={color}\n                  onSelect={() => onSelect(color)}\n                  className=\"flex items-center justify-between gap-2 cursor-pointer\"\n                >\n                  {color !== loadingColor && (\n                    <div className=\"p-1\">{fmt_upper_camelcase(color)}</div>\n                  )}\n                  {color === loadingColor && (\n                    <Loader2 className=\"w-4 h-4 animate-spin mx-1\" />\n                  )}\n                  <div\n                    className={cn(\n                      \"w-[25px] h-[25px] rounded-sm bg-opacity-70\",\n                      tag_background_class(color)\n                    )}\n                  />\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/settings/users.tsx",
    "content": "import { ExportButton } from \"@components/export\";\nimport { Section } from \"@components/layouts\";\nimport { DeleteUserGroup } from \"@components/users/delete-user-group\";\nimport {\n  NewLocalUser,\n  NewServiceUser,\n  NewUserGroup,\n} from \"@components/users/new\";\nimport { UserTable } from \"@components/users/table\";\nimport {\n  useInvalidate,\n  useLoginOptions,\n  useRead,\n  useSetTitle,\n  useUser,\n  useWrite,\n} from \"@lib/hooks\";\nimport { filterBySplit } from \"@lib/utils\";\nimport { DataTable } from \"@ui/data-table\";\nimport { Input } from \"@ui/input\";\nimport { useToast } from \"@ui/use-toast\";\nimport { Search, User, Users } from \"lucide-react\";\nimport React, { useState } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\n\nexport const UsersPage = ({ goToProfile }: { goToProfile: () => void }) => {\n  useSetTitle(\"Users\");\n  const [search, setSearch] = useState(\"\");\n  return (\n    <div className=\"flex flex-col gap-6\">\n      <UserGroupsSection search={search} setSearch={setSearch} />\n      <UsersSection search={search} goToProfile={goToProfile} />\n    </div>\n  );\n};\n\nconst UserGroupsSection = ({\n  search,\n  setSearch,\n}: {\n  search: string;\n  setSearch: React.Dispatch<React.SetStateAction<string>>;\n}) => {\n  const nav = useNavigate();\n  const groups = useRead(\"ListUserGroups\", {}).data;\n  const filtered = filterBySplit(groups, search, (group) => group.name);\n  return (\n    <Section title=\"User Groups\" icon={<Users className=\"w-4 h-4\" />}>\n      <div className=\"flex items-center justify-between\">\n        <NewUserGroup />\n        <div className=\"flex items-center gap-4\">\n          {groups && groups.length > 0 && (\n            <div className=\"flex items-center gap-4\">\n              <ExportButton\n                user_groups={groups\n                  ?.map((group) => group._id?.$oid!)\n                  .filter((id) => id)}\n              />\n            </div>\n          )}\n          <div className=\"relative\">\n            <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n            <Input\n              placeholder=\"search...\"\n              value={search}\n              onChange={(e) => setSearch(e.target.value)}\n              className=\"pl-8 w-[200px] lg:w-[300px]\"\n            />\n          </div>\n        </div>\n      </div>\n      <DataTable\n        tableKey=\"user-groups\"\n        data={filtered}\n        columns={[\n          { header: \"Name\", accessorKey: \"name\" },\n          {\n            header: \"Members\",\n            accessorFn: (group) =>\n              group.everyone ? \"Everyone\" : (group.users ?? []).length,\n          },\n          {\n            header: \"Delete\",\n            cell: ({ row: { original: group } }) => (\n              <DeleteUserGroup group={group} />\n            ),\n          },\n        ]}\n        onRowClick={(group) => nav(`/user-groups/${group._id!.$oid}`)}\n      />\n    </Section>\n  );\n};\n\nconst UsersSection = ({\n  goToProfile,\n  search,\n}: {\n  goToProfile: () => void;\n  search: string;\n}) => {\n  const user = useUser().data;\n  const inv = useInvalidate();\n  const { toast } = useToast();\n  const local_login_enabled = useLoginOptions().data?.local;\n  const { mutate: deleteUser } = useWrite(\"DeleteUser\", {\n    onSuccess: () => {\n      toast({ title: \"User deleted.\" });\n      inv([\"ListUsers\"]);\n    },\n  });\n  const users = useRead(\"ListUsers\", {}).data;\n  const filtered = filterBySplit(users, search, (user) => user.username);\n  return (\n    <Section title=\"Users\" icon={<User className=\"w-4 h-4\" />}>\n      <div className=\"flex items-center gap-4\">\n        {local_login_enabled && <NewLocalUser />}\n        <NewServiceUser />\n      </div>\n      <UserTable\n        users={filtered}\n        onUserDelete={\n          user?.admin ? (user_id) => deleteUser({ user: user_id }) : undefined\n        }\n        userDeleteDisabled={(user_id) => {\n          const toDelete = users?.find((user) => user._id?.$oid === user_id);\n          if (!toDelete) return true;\n          if (!toDelete.admin) return false;\n          if (toDelete.super_admin) return true;\n          return !user?.super_admin;\n        }}\n        onSelfClick={goToProfile}\n      />\n    </Section>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/settings/variables.tsx",
    "content": "import {\n  ConfirmButton,\n  CopyButton,\n  TextUpdateMenuMonaco,\n} from \"@components/util\";\nimport {\n  useInvalidate,\n  useRead,\n  useSetTitle,\n  useUser,\n  useWrite,\n} from \"@lib/hooks\";\nimport { Badge } from \"@ui/badge\";\nimport { Button } from \"@ui/button\";\nimport { Card } from \"@ui/card\";\nimport { DataTable, SortableHeader } from \"@ui/data-table\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@ui/dialog\";\nimport { Input } from \"@ui/input\";\nimport { Switch } from \"@ui/switch\";\nimport { useToast } from \"@ui/use-toast\";\nimport { Check, Loader2, PlusCircle, Search, Trash } from \"lucide-react\";\nimport { useState } from \"react\";\n\nexport const Variables = () => {\n  const user = useUser().data;\n  const disabled = !user?.admin;\n  useSetTitle(\"Variables\");\n  const [updateMenuData, setUpdateMenuData] = useState<\n    | false\n    | {\n        title: string;\n        value: string;\n        placeholder: string;\n        onUpdate: (value: string) => void;\n      }\n  >(false);\n  const [search, setSearch] = useState(\"\");\n  const variables = useRead(\"ListVariables\", {}).data ?? [];\n  const secrets = useRead(\"ListSecrets\", {}).data ?? [];\n  const searchSplit = search?.toLowerCase().split(\" \") || [];\n  const filtered =\n    variables?.filter((variable) => {\n      if (searchSplit.length > 0) {\n        const name = variable.name.toLowerCase();\n        return searchSplit.every((search) => name.includes(search));\n      } else return true;\n    }) ?? [];\n  const { toast } = useToast();\n  const inv = useInvalidate();\n  const { mutate: updateValue } = useWrite(\"UpdateVariableValue\", {\n    onSuccess: () => {\n      inv([\"ListVariables\"], [\"GetVariable\"]);\n      toast({ title: \"Updated variable value\" });\n    },\n  });\n  const { mutate: updateDescription } = useWrite(\"UpdateVariableDescription\", {\n    onSuccess: () => {\n      inv([\"ListVariables\"], [\"GetVariable\"]);\n      toast({ title: \"Updated variable description\" });\n    },\n  });\n  const { mutate: updateIsSecret } = useWrite(\"UpdateVariableIsSecret\", {\n    onSuccess: () => {\n      inv([\"ListVariables\"], [\"GetVariable\"]);\n      toast({ title: \"Updated variable 'is secret'\" });\n    },\n  });\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex justify-between gap-4\">\n        <CreateVariable />\n        <div className=\"relative\">\n          <Search className=\"w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground\" />\n          <Input\n            placeholder=\"search...\"\n            value={search}\n            onChange={(e) => setSearch(e.target.value)}\n            className=\"pl-8 w-[200px] lg:w-[300px]\"\n          />\n        </div>\n      </div>\n\n      {updateMenuData && (\n        <TextUpdateMenuMonaco\n          title={updateMenuData.title}\n          placeholder={updateMenuData.placeholder}\n          value={updateMenuData.value}\n          onUpdate={updateMenuData.onUpdate}\n          triggerClassName=\"w-full\"\n          disabled={disabled}\n          open={!!updateMenuData}\n          setOpen={(open) => {\n            if (!open) {\n              setUpdateMenuData(false);\n            }\n          }}\n          triggerHidden\n        />\n      )}\n\n      {/** VARIABLES */}\n      <div className=\"max-w-full overflow-auto\">\n        {/* <div className=\"w-full min-w-[1200px]\"> */}\n        <DataTable\n          tableKey=\"variables\"\n          data={filtered}\n          columns={[\n            {\n              accessorKey: \"name\",\n              size: 200,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Name\" />\n              ),\n            },\n            {\n              accessorKey: \"value\",\n              size: 300,\n              header: ({ column }) => (\n                <SortableHeader column={column} title=\"Value\" />\n              ),\n              cell: ({ row }) => {\n                const valueDisplay = row.original.is_secret\n                  ? \"*\".repeat(row.original.value?.length || 0)\n                  : row.original.value;\n                return (\n                  <div className=\"flex items-center gap-2\">\n                    <Card\n                      className=\"w-full max-w-[200px] xl:max-w-full px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer text-sm text-nowrap overflow-hidden overflow-ellipsis text-muted-foreground\"\n                      onClick={() => {\n                        setUpdateMenuData({\n                          title: `${row.original.name} - Value`,\n                          value: row.original.value ?? \"\",\n                          placeholder: \"Set value\",\n                          onUpdate: (value) => {\n                            if (row.original.value === value) {\n                              return;\n                            }\n                            updateValue({ name: row.original.name, value });\n                          },\n                        });\n                      }}\n                    >\n                      {valueDisplay || \"Set value\"}\n                    </Card>\n                    <CopyButton content={row.original.value} />\n                  </div>\n                );\n              },\n            },\n            {\n              accessorKey: \"description\",\n              size: 200,\n              header: \"Description\",\n              cell: ({ row }) => {\n                return (\n                  <Card\n                    className=\"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer w-full\"\n                    onClick={() => {\n                      setUpdateMenuData({\n                        title: `${row.original.name} - Description`,\n                        value: row.original.description ?? \"\",\n                        placeholder: \"Set description\",\n                        onUpdate: (description) => {\n                          if (row.original.description === description) {\n                            return;\n                          }\n                          updateDescription({\n                            name: row.original.name,\n                            description,\n                          });\n                        },\n                      });\n                    }}\n                  >\n                    <div className=\"text-sm text-nowrap overflow-hidden overflow-ellipsis w-full text-muted-foreground\">\n                      {row.original.description || \"Set description\"}\n                    </div>\n                  </Card>\n                );\n              },\n            },\n            {\n              header: \"Secret\",\n              size: 100,\n              cell: ({ row }) => (\n                <Switch\n                  checked={row.original.is_secret}\n                  onCheckedChange={(is_secret) =>\n                    updateIsSecret({ name: row.original.name, is_secret })\n                  }\n                  disabled={disabled}\n                />\n              ),\n            },\n            {\n              header: \"Delete\",\n              size: 200,\n              cell: ({ row }) => <DeleteVariable name={row.original.name} />,\n            },\n          ]}\n        />\n        {/* </div> */}\n      </div>\n\n      {/** SECRETS */}\n      {secrets.length ? (\n        <div className=\"flex items-center gap-2 flex-wrap text-muted-foreground\">\n          <div>Core Secrets:</div>\n          {secrets.map((secret) => (\n            <Badge variant=\"secondary\">{secret}</Badge>\n          ))}\n        </div>\n      ) : undefined}\n    </div>\n  );\n};\n\nconst CreateVariable = () => {\n  const { toast } = useToast();\n  const [open, setOpen] = useState(false);\n  const [name, setName] = useState(\"\");\n  const invalidate = useInvalidate();\n  const { mutate, isPending } = useWrite(\"CreateVariable\", {\n    onSuccess: () => {\n      invalidate([\"ListVariables\"], [\"GetVariable\"]);\n      toast({ title: \"Variable Created\" });\n      setOpen(false);\n    },\n  });\n  const user = useUser().data;\n  const disabled = !user?.admin;\n  const submit = () => mutate({ name });\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button\n          variant=\"secondary\"\n          className=\"items-center gap-2\"\n          disabled={disabled}\n        >\n          New Variable <PlusCircle className=\"w-4 h-4\" />\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Create Variable</DialogTitle>\n        </DialogHeader>\n        <div className=\"py-8 flex flex-col gap-4\">\n          <div className=\"flex items-center justify-between\">\n            Name\n            <Input\n              className=\"w-72\"\n              value={name}\n              onChange={(e) =>\n                setName(e.target.value.toUpperCase().replaceAll(\" \", \"_\"))\n              }\n              placeholder=\"Input variable name\"\n            />\n          </div>\n        </div>\n        <DialogFooter className=\"flex justify-end\">\n          <Button className=\"gap-4\" onClick={submit} disabled={isPending}>\n            Submit\n            {isPending ? (\n              <Loader2 className=\"w-4 animate-spin\" />\n            ) : (\n              <Check className=\"w-4\" />\n            )}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst DeleteVariable = ({ name }: { name: string }) => {\n  const invalidate = useInvalidate();\n  const { toast } = useToast();\n  const { mutate, isPending } = useWrite(\"DeleteVariable\", {\n    onSuccess: () => {\n      invalidate([\"ListVariables\"], [\"GetVariable\"]);\n      toast({ title: \"Variable deleted\" });\n    },\n  });\n  return (\n    <ConfirmButton\n      title=\"Delete\"\n      icon={<Trash className=\"w-4 h-4\" />}\n      onClick={() => mutate({ name })}\n      loading={isPending}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/stack-service/index.tsx",
    "content": "import { Section } from \"@components/layouts\";\nimport {\n  ResourceDescription,\n  ResourceLink,\n  ResourcePageHeader,\n} from \"@components/resources/common\";\nimport { useStack } from \"@components/resources/stack\";\nimport {\n  DeployStack,\n  DestroyStack,\n  PauseUnpauseStack,\n  PullStack,\n  RestartStack,\n  StartStopStack,\n} from \"@components/resources/stack/actions\";\nimport {\n  container_state_intention,\n  stroke_color_class_by_intention,\n} from \"@lib/color\";\nimport {\n  usePermissions,\n  useLocalStorage,\n  useRead,\n  useSetTitle,\n  useContainerPortsMap,\n} from \"@lib/hooks\";\nimport { cn } from \"@lib/utils\";\nimport { ConnectExecQuery, Types } from \"komodo_client\";\nimport { ChevronLeft, Clapperboard, Layers2 } from \"lucide-react\";\nimport { Link, useParams } from \"react-router-dom\";\nimport { StackServiceLogs } from \"./log\";\nimport { Button } from \"@ui/button\";\nimport { ExportButton } from \"@components/export\";\nimport { ContainerPortLink, DockerResourceLink } from \"@components/util\";\nimport { ResourceNotifications } from \"@pages/resource-notifications\";\nimport { Fragment } from \"react/jsx-runtime\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@ui/tabs\";\nimport { ContainerTerminal } from \"@components/terminal/container\";\nimport { useServer } from \"@components/resources/server\";\nimport { StackServiceInspect } from \"./inspect\";\nimport { useMemo } from \"react\";\n\ntype IdServiceComponent = React.FC<{ id: string; service?: string }>;\n\nconst Actions: { [action: string]: IdServiceComponent } = {\n  DeployStack,\n  PullStack,\n  RestartStack,\n  PauseUnpauseStack,\n  StartStopStack,\n  DestroyStack,\n};\n\nexport default function StackServicePage() {\n  const { type, id, service } = useParams() as {\n    type: string;\n    id: string;\n    service: string;\n  };\n  if (type !== \"stacks\") {\n    return <div>This resource type does not have any services.</div>;\n  }\n  return <StackServicePageInner stack_id={id} service={service} />;\n}\n\nconst StackServicePageInner = ({\n  stack_id,\n  service,\n}: {\n  stack_id: string;\n  service: string;\n}) => {\n  const stack = useStack(stack_id);\n  useSetTitle(`${stack?.name} | ${service}`);\n  const { canExecute, canWrite } = usePermissions({\n    type: \"Stack\",\n    id: stack_id,\n  });\n  const services = useRead(\"ListStackServices\", { stack: stack_id }).data;\n  const container = services?.find((s) => s.service === service)?.container;\n  const ports_map = useContainerPortsMap(container?.ports ?? []);\n  const state = container?.state ?? Types.ContainerStateStatusEnum.Empty;\n  const intention = container_state_intention(state);\n  const stroke_color = stroke_color_class_by_intention(intention);\n\n  return (\n    <div>\n      <div className=\"w-full flex items-center justify-between mb-12\">\n        <Link to={\"/stacks/\" + stack_id}>\n          <Button className=\"gap-2\" variant=\"secondary\">\n            <ChevronLeft className=\"w-4\" />\n            Back\n          </Button>\n        </Link>\n        <div className=\"flex items-center gap-4\">\n          <ExportButton targets={[{ type: \"Stack\", id: stack_id }]} />\n        </div>\n      </div>\n      <div className=\"flex flex-col xl:flex-row gap-4\">\n        {/** HEADER */}\n        <div className=\"w-full flex flex-col gap-4\">\n          <div className=\"flex flex-col gap-2 border rounded-md\">\n            {/* <Components.ResourcePageHeader id={id} /> */}\n            <ResourcePageHeader\n              type={undefined}\n              id={undefined}\n              intent={intention}\n              icon={<Layers2 className={cn(\"w-8 h-8\", stroke_color)} />}\n              resource={undefined}\n              name={service}\n              state={state}\n              status={container?.status}\n            />\n            <div className=\"flex flex-col pb-2 px-4\">\n              <div className=\"flex items-center gap-x-4 gap-y-0 flex-wrap text-muted-foreground\">\n                <ResourceLink type=\"Stack\" id={stack_id} />\n                {stack?.info.server_id && (\n                  <>\n                    |\n                    <ResourceLink type=\"Server\" id={stack.info.server_id} />\n                  </>\n                )}\n                {stack?.info.server_id && container?.name && (\n                  <>\n                    |\n                    <DockerResourceLink\n                      type=\"container\"\n                      server_id={stack.info.server_id}\n                      name={container.name}\n                      muted\n                    />\n                  </>\n                )}\n                {stack?.info.server_id && container?.image && (\n                  <>\n                    |\n                    <DockerResourceLink\n                      type=\"image\"\n                      server_id={stack.info.server_id}\n                      name={container.image}\n                      id={container.image_id}\n                      muted\n                    />\n                  </>\n                )}\n                {stack?.info.server_id &&\n                  container?.networks?.map((network) => (\n                    <Fragment key={network}>\n                      |\n                      <DockerResourceLink\n                        type=\"network\"\n                        server_id={stack.info.server_id}\n                        name={network}\n                        muted\n                      />\n                    </Fragment>\n                  ))}\n                {stack?.info.server_id &&\n                  container &&\n                  container.volumes?.map((volume) => (\n                    <Fragment key={volume}>\n                      |\n                      <DockerResourceLink\n                        type=\"volume\"\n                        server_id={stack.info.server_id}\n                        name={volume}\n                        muted\n                      />\n                    </Fragment>\n                  ))}\n                {stack?.info.server_id &&\n                  Object.keys(ports_map).map((host_port) => (\n                    <Fragment key={host_port}>\n                      |\n                      <ContainerPortLink\n                        host_port={host_port}\n                        ports={ports_map[host_port]}\n                        server_id={stack.info.server_id}\n                      />\n                    </Fragment>\n                  ))}\n              </div>\n            </div>\n          </div>\n          <ResourceDescription\n            type=\"Stack\"\n            id={stack_id}\n            disabled={!canWrite}\n          />\n        </div>\n        {/** NOTIFICATIONS */}\n        <ResourceNotifications type=\"Stack\" id={stack_id} />\n      </div>\n\n      <div className=\"mt-8 flex flex-col gap-12\">\n        {/* Actions */}\n        {canExecute && (\n          <Section\n            title=\"Actions (Service)\"\n            icon={<Clapperboard className=\"w-4 h-4\" />}\n          >\n            <div className=\"flex gap-4 items-center flex-wrap\">\n              {Object.entries(Actions).map(([key, Action]) => (\n                <Action key={key} id={stack_id} service={service} />\n              ))}\n            </div>\n          </Section>\n        )}\n\n        {/* Tabs */}\n        <div className=\"pt-4\">\n          {stack && (\n            <StackServiceTabs\n              stack={stack}\n              service={service}\n              container_state={state}\n            />\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst StackServiceTabs = ({\n  stack,\n  service,\n  container_state,\n}: {\n  stack: Types.StackListItem;\n  service: string;\n  container_state: Types.ContainerStateStatusEnum;\n}) => {\n  const [_view, setView] = useLocalStorage<\"Log\" | \"Inspect\" | \"Terminal\">(\n    `stack-${stack.id}-${service}-tabs-v1`,\n    \"Log\"\n  );\n  const { specificLogs, specificInspect, specificTerminal } = usePermissions({\n    type: \"Stack\",\n    id: stack.id,\n  });\n  const container_exec_disabled =\n    useServer(stack.info.server_id)?.info.container_exec_disabled ?? true;\n  const logDisabled =\n    !specificLogs || container_state === Types.ContainerStateStatusEnum.Empty;\n  const inspectDisabled =\n    !specificInspect ||\n    container_state === Types.ContainerStateStatusEnum.Empty;\n  const terminalDisabled =\n    !specificTerminal ||\n    container_exec_disabled ||\n    container_state !== Types.ContainerStateStatusEnum.Running;\n  const view =\n    (inspectDisabled && _view === \"Inspect\") ||\n    (terminalDisabled && _view === \"Terminal\")\n      ? \"Log\"\n      : _view;\n  const tabs = useMemo(\n    () => (\n      <TabsList className=\"justify-start w-fit\">\n        <TabsTrigger value=\"Log\" className=\"w-[110px]\" disabled={logDisabled}>\n          Log\n        </TabsTrigger>\n        {specificInspect && (\n          <TabsTrigger\n            value=\"Inspect\"\n            className=\"w-[110px]\"\n            disabled={inspectDisabled}\n          >\n            Inspect\n          </TabsTrigger>\n        )}\n        {specificTerminal && (\n          <TabsTrigger\n            value=\"Terminal\"\n            className=\"w-[110px]\"\n            disabled={terminalDisabled}\n          >\n            Terminal\n          </TabsTrigger>\n        )}\n      </TabsList>\n    ),\n    [\n      logDisabled,\n      specificInspect,\n      inspectDisabled,\n      specificTerminal,\n      terminalDisabled,\n    ]\n  );\n  const terminalQuery = useMemo(\n    () =>\n      ({\n        type: \"stack\",\n        query: {\n          stack: stack.id,\n          service,\n          // This is handled inside ContainerTerminal\n          shell: \"\",\n        },\n      }) as ConnectExecQuery,\n    [stack.id, service]\n  );\n  return (\n    <Tabs value={view} onValueChange={setView as any}>\n      <TabsContent value=\"Log\">\n        <StackServiceLogs\n          id={stack.id}\n          service={service}\n          titleOther={tabs}\n          disabled={logDisabled}\n        />\n      </TabsContent>\n      <TabsContent value=\"Inspect\">\n        <StackServiceInspect\n          id={stack.id}\n          service={service}\n          titleOther={tabs}\n        />\n      </TabsContent>\n      <TabsContent value=\"Terminal\">\n        <ContainerTerminal query={terminalQuery} titleOther={tabs} />\n      </TabsContent>\n    </Tabs>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/stack-service/inspect.tsx",
    "content": "import { usePermissions, useRead } from \"@lib/hooks\";\nimport { ReactNode } from \"react\";\nimport { Types } from \"komodo_client\";\nimport { Section } from \"@components/layouts\";\nimport { InspectContainerView } from \"@components/inspect\";\n\nexport const StackServiceInspect = ({\n  id,\n  service,\n  titleOther,\n}: {\n  id: string;\n  service: string;\n  titleOther: ReactNode;\n}) => {\n  const { specific } = usePermissions({ type: \"Stack\", id });\n  if (!specific.includes(Types.SpecificPermission.Inspect)) {\n    return (\n      <Section titleOther={titleOther}>\n        <div className=\"min-h-[60vh]\">\n          <h1>User does not have permission to inspect this Stack service.</h1>\n        </div>\n      </Section>\n    );\n  }\n  return (\n    <Section titleOther={titleOther}>\n      <StackServiceInspectInner id={id} service={service} />\n    </Section>\n  );\n};\n\nconst StackServiceInspectInner = ({\n  id,\n  service,\n}: {\n  id: string;\n  service: string;\n}) => {\n  const {\n    data: container,\n    error,\n    isPending,\n    isError,\n  } = useRead(\"InspectStackContainer\", {\n    stack: id,\n    service,\n  });\n  return (\n    <InspectContainerView\n      container={container}\n      error={error}\n      isPending={isPending}\n      isError={isError}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/stack-service/log.tsx",
    "content": "import { useRead } from \"@lib/hooks\";\nimport { Types } from \"komodo_client\";\nimport { Log, LogSection } from \"@components/log\";\nimport { ReactNode } from \"react\";\nimport { Section } from \"@components/layouts\";\n\nexport const StackServiceLogs = ({\n  id,\n  service,\n  titleOther,\n  disabled,\n}: {\n  /// Stack id\n  id: string;\n  service: string;\n  titleOther?: ReactNode;\n  disabled: boolean;\n}) => {\n  // const stack = useStack(id);\n  const services = useRead(\"ListStackServices\", { stack: id }).data;\n  const container = services?.find((s) => s.service === service)?.container;\n  const state = container?.state ?? Types.ContainerStateStatusEnum.Empty;\n\n  if (\n    disabled ||\n    state === undefined ||\n    state === Types.ContainerStateStatusEnum.Empty\n  ) {\n    return (\n      <Section titleOther={titleOther}>\n        <h1>Logs are disabled.</h1>\n      </Section>\n    );\n  }\n\n  return <StackLogsInner titleOther={titleOther} id={id} service={service} />;\n};\n\nconst StackLogsInner = ({\n  id,\n  service,\n  titleOther,\n}: {\n  /// Stack id\n  id: string;\n  service: string;\n  titleOther?: ReactNode;\n}) => {\n  return (\n    <LogSection\n      titleOther={titleOther}\n      regular_logs={(timestamps, stream, tail, poll) =>\n        NoSearchLogs(id, service, tail, timestamps, stream, poll)\n      }\n      search_logs={(timestamps, terms, invert, poll) =>\n        SearchLogs(id, service, terms, invert, timestamps, poll)\n      }\n    />\n  );\n};\n\nconst NoSearchLogs = (\n  id: string,\n  service: string,\n  tail: number,\n  timestamps: boolean,\n  stream: string,\n  poll: boolean\n) => {\n  const { data: log, refetch } = useRead(\n    \"GetStackLog\",\n    {\n      stack: id,\n      services: [service],\n      tail,\n      timestamps,\n    },\n    { refetchInterval: poll ? 3000 : false }\n  );\n  return {\n    Log: (\n      <div className=\"relative\">\n        <Log log={log} stream={stream as \"stdout\" | \"stderr\"} />\n      </div>\n    ),\n    refetch,\n    stderr: !!log?.stderr,\n  };\n};\n\nconst SearchLogs = (\n  id: string,\n  service: string,\n  terms: string[],\n  invert: boolean,\n  timestamps: boolean,\n  poll: boolean\n) => {\n  const { data: log, refetch } = useRead(\n    \"SearchStackLog\",\n    {\n      stack: id,\n      services: [service],\n      terms,\n      combinator: Types.SearchCombinator.And,\n      invert,\n      timestamps,\n    },\n    { refetchInterval: poll ? 10000 : false }\n  );\n  return {\n    Log: (\n      <div className=\"h-full relative\">\n        <Log log={log} stream=\"stdout\" />\n      </div>\n    ),\n    refetch,\n    stderr: !!log?.stderr,\n  };\n};\n"
  },
  {
    "path": "frontend/src/pages/update.tsx",
    "content": "import { UpdateDetailsInner } from \"@components/updates/details\";\nimport { useRead, useSetTitle } from \"@lib/hooks\";\nimport { To, useLocation, useNavigate, useParams } from \"react-router-dom\";\n\nexport default function UpdatePage() {\n  useSetTitle(\"Update\");\n  // https://github.com/remix-run/react-router/discussions/9788#discussioncomment-4604278\n  const navTo = (useLocation().key === \"default\" ? \"/\" : -1) as To;\n  const navigate = useNavigate();\n  const id = useParams().id as string;\n  const update = useRead(\"GetUpdate\", { id }).data;\n\n  if (!update) return null;\n\n  return <UpdateDetailsInner id={id} open setOpen={() => navigate(navTo)} />;\n}\n"
  },
  {
    "path": "frontend/src/pages/updates.tsx",
    "content": "import { Page } from \"@components/layouts\";\nimport { ResourceComponents } from \"@components/resources\";\nimport { UpdatesTable } from \"@components/updates/table\";\nimport { useRead, useSetTitle } from \"@lib/hooks\";\nimport { filterBySplit, RESOURCE_TARGETS } from \"@lib/utils\";\nimport { Types } from \"komodo_client\";\nimport { CaretSortIcon } from \"@radix-ui/react-icons\";\nimport { UsableResource } from \"@types\";\nimport { Button } from \"@ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport {\n  Bell,\n  Box,\n  ChevronLeft,\n  ChevronRight,\n  MinusCircle,\n  SearchX,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n} from \"@ui/select\";\nimport { ResourceSelector } from \"@components/resources/common\";\n\nexport default function UpdatesPage() {\n  useSetTitle(\"Updates\");\n  const [page, setPage] = useState(0);\n  const [params, setParams] = useSearchParams();\n\n  const { type, id, operation } = {\n    type: (params.get(\"type\") as UsableResource) ?? undefined,\n    id: params.get(\"id\") ?? undefined,\n    operation: (params.get(\"operation\") as Types.Operation) ?? undefined,\n  };\n\n  const { data: updates } = useRead(\"ListUpdates\", {\n    query: { \"target.type\": type, \"target.id\": id, operation },\n    page,\n  });\n\n  return (\n    <Page\n      title=\"Updates\"\n      icon={<Bell className=\"w-8\" />}\n      actions={\n        <>\n          <div className=\"flex items-center md:justify-end gap-4 flex-wrap\">\n            {/* resource type */}\n            <Select\n              value={type ?? \"all\"}\n              onValueChange={(type) => {\n                const p = new URLSearchParams(params.toString());\n                type === \"all\" ? p.delete(\"type\") : p.set(\"type\", type);\n                p.delete(\"id\");\n                p.delete(\"operation\");\n                setParams(p);\n              }}\n            >\n              <SelectTrigger className=\"w-48\">\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"all\">\n                  <div className=\"flex items-center gap-2\">\n                    <Box className=\"w-4 text-muted-foreground\" />\n                    All Resources\n                  </div>\n                </SelectItem>\n                <SelectSeparator />\n                {RESOURCE_TARGETS.map((type) => {\n                  const Icon = ResourceComponents[type].Icon;\n                  return (\n                    <SelectItem key={type} value={type}>\n                      <div className=\"flex items-center gap-2\">\n                        <span className=\"text-muted-foreground\">\n                          <Icon />\n                        </span>\n                        {type}\n                      </div>\n                    </SelectItem>\n                  );\n                })}\n              </SelectContent>\n            </Select>\n\n            {/* resource id */}\n            {type && (\n              <ResourceSelector\n                type={type}\n                selected={id}\n                onSelect={(id) => {\n                  const p = new URLSearchParams(params.toString());\n                  id === \"all\" ? p.delete(\"id\") : p.set(\"id\", id);\n                  setParams(p);\n                }}\n              />\n            )}\n\n            {/* operation */}\n            <OperationSelector\n              selected={operation}\n              options={type && OPERATIONS_BY_RESOURCE[type]}\n              onSelect={(op) => {\n                const p = new URLSearchParams(params.toString());\n                op ? p.set(\"operation\", op) : p.delete(\"operation\");\n                setParams(p);\n              }}\n            />\n\n            {/* reset */}\n            <Button\n              size=\"icon\"\n              onClick={() => setParams({})}\n              variant=\"secondary\"\n            >\n              <MinusCircle className=\"w-4\" />\n            </Button>\n          </div>\n        </>\n      }\n    >\n      <div className=\"flex flex-col gap-2\">\n        <UpdatesTable\n          updates={updates?.updates ?? []}\n          showTarget={!params.get(\"id\")}\n        />\n        <div className=\"flex gap-4 items-center\">\n          <Button\n            variant=\"outline\"\n            onClick={() => setPage(page - 1)}\n            disabled={page === 0}\n            size=\"icon\"\n          >\n            <ChevronLeft className=\"w-4\" />\n          </Button>\n          {Array.from(new Array(page + 1)).map((_, i) => (\n            <Button\n              key={i}\n              onClick={() => setPage(i)}\n              variant={page === i ? \"secondary\" : \"outline\"}\n            >\n              {i + 1}\n            </Button>\n          ))}\n          {/* Page: {page + 1} */}\n          <Button\n            variant=\"outline\"\n            onClick={() => updates?.next_page && setPage(updates.next_page)}\n            disabled={!updates?.next_page}\n            size=\"icon\"\n          >\n            <ChevronRight className=\"w-4\" />\n          </Button>\n        </div>\n      </div>\n    </Page>\n  );\n}\n\nconst OPERATIONS_BY_RESOURCE: { [key: string]: Types.Operation[] } = {\n  Server: [\n    Types.Operation.CreateServer,\n    Types.Operation.UpdateServer,\n    Types.Operation.DeleteServer,\n    Types.Operation.RenameServer,\n    Types.Operation.StartContainer,\n    Types.Operation.RestartContainer,\n    Types.Operation.PauseContainer,\n    Types.Operation.UnpauseContainer,\n    Types.Operation.StopContainer,\n    Types.Operation.DestroyContainer,\n    Types.Operation.StartAllContainers,\n    Types.Operation.RestartAllContainers,\n    Types.Operation.PauseAllContainers,\n    Types.Operation.UnpauseAllContainers,\n    Types.Operation.StopAllContainers,\n    Types.Operation.PruneContainers,\n    Types.Operation.CreateNetwork,\n    Types.Operation.DeleteNetwork,\n    Types.Operation.PruneNetworks,\n    Types.Operation.DeleteImage,\n    Types.Operation.PruneImages,\n    Types.Operation.DeleteVolume,\n    Types.Operation.PruneVolumes,\n    Types.Operation.PruneDockerBuilders,\n    Types.Operation.PruneBuildx,\n    Types.Operation.PruneSystem,\n  ],\n  Stack: [\n    Types.Operation.CreateStack,\n    Types.Operation.UpdateStack,\n    Types.Operation.RenameStack,\n    Types.Operation.DeleteStack,\n    Types.Operation.WriteStackContents,\n    Types.Operation.RefreshStackCache,\n    Types.Operation.DeployStack,\n    Types.Operation.StartStack,\n    Types.Operation.RestartStack,\n    Types.Operation.PauseStack,\n    Types.Operation.UnpauseStack,\n    Types.Operation.StopStack,\n    Types.Operation.DestroyStack,\n    Types.Operation.StartStackService,\n    Types.Operation.RestartStackService,\n    Types.Operation.PauseStackService,\n    Types.Operation.UnpauseStackService,\n    Types.Operation.StopStackService,\n  ],\n  Deployment: [\n    Types.Operation.CreateDeployment,\n    Types.Operation.UpdateDeployment,\n    Types.Operation.DeleteDeployment,\n    Types.Operation.Deploy,\n    Types.Operation.StartDeployment,\n    Types.Operation.RestartDeployment,\n    Types.Operation.PauseDeployment,\n    Types.Operation.UnpauseDeployment,\n    Types.Operation.StopDeployment,\n    Types.Operation.DestroyDeployment,\n    Types.Operation.RenameDeployment,\n  ],\n  Build: [\n    Types.Operation.CreateBuild,\n    Types.Operation.UpdateBuild,\n    Types.Operation.DeleteBuild,\n    Types.Operation.RunBuild,\n    Types.Operation.CancelBuild,\n  ],\n  Repo: [\n    Types.Operation.CreateRepo,\n    Types.Operation.UpdateRepo,\n    Types.Operation.DeleteRepo,\n    Types.Operation.CloneRepo,\n    Types.Operation.PullRepo,\n    Types.Operation.BuildRepo,\n    Types.Operation.CancelRepoBuild,\n  ],\n  Procedure: [\n    Types.Operation.CreateProcedure,\n    Types.Operation.UpdateProcedure,\n    Types.Operation.DeleteProcedure,\n    Types.Operation.RunProcedure,\n  ],\n  Builder: [\n    Types.Operation.CreateBuilder,\n    Types.Operation.UpdateBuilder,\n    Types.Operation.DeleteBuilder,\n  ],\n  Alerter: [\n    Types.Operation.CreateAlerter,\n    Types.Operation.UpdateAlerter,\n    Types.Operation.DeleteAlerter,\n  ],\n  ResourceSync: [\n    Types.Operation.CreateResourceSync,\n    Types.Operation.UpdateResourceSync,\n    Types.Operation.DeleteResourceSync,\n    Types.Operation.CommitSync,\n    Types.Operation.RunSync,\n  ],\n};\n\nconst OperationSelector = ({\n  selected,\n  onSelect,\n  options = Object.values(Types.Operation).filter(\n    (o) => o !== Types.Operation.None\n  ),\n}: {\n  selected: Types.Operation | undefined;\n  onSelect: (operation: Types.Operation | undefined) => void;\n  options?: Types.Operation[];\n}) => {\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n  const filtered = filterBySplit(options, search, (item) => item);\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <div className=\"h-full w-[200px] cursor-pointer flex items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\">\n          {selected ?? \"Select Operation\"}\n          <CaretSortIcon className=\"h-4 w-4 opacity-50\" />\n        </div>\n      </PopoverTrigger>\n      <PopoverContent align=\"end\" className=\"w-[200px] max-h-[200px] p-0\">\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Search Operations\"\n            value={search}\n            onValueChange={setSearch}\n            className=\"h-9\"\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center\">\n              No Operations Found\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n\n            <CommandGroup>\n              <CommandItem\n                className=\"cursor-pointer\"\n                onSelect={() => {\n                  onSelect(undefined);\n                  setOpen(false);\n                }}\n              >\n                <div>All</div>\n              </CommandItem>\n\n              {filtered.map((operation) => (\n                <CommandItem\n                  key={operation}\n                  className=\"cursor-pointer\"\n                  onSelect={() => {\n                    onSelect(operation);\n                    setOpen(false);\n                  }}\n                >\n                  {operation}\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/user-group.tsx",
    "content": "import { ExportButton } from \"@components/export\";\nimport { Page, Section } from \"@components/layouts\";\nimport { PermissionsTableTabs } from \"@components/users/permissions-table\";\nimport { UserTable } from \"@components/users/table\";\nimport { useInvalidate, useRead, useWrite } from \"@lib/hooks\";\nimport { filterBySplit } from \"@lib/utils\";\nimport { Button } from \"@ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@ui/command\";\nimport { Input } from \"@ui/input\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@ui/popover\";\nimport { useToast } from \"@ui/use-toast\";\nimport { PlusCircle, Save, SearchX, User, Users } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport { Switch } from \"@ui/switch\";\nimport { DeleteUserGroup } from \"@components/users/delete-user-group\";\n\nexport default function UserGroupPage() {\n  const { toast } = useToast();\n  const inv = useInvalidate();\n  const group_id = useParams().id as string;\n  const group = useRead(\"ListUserGroups\", {}).data?.find(\n    (group) => group._id?.$oid === group_id\n  );\n  const users = useRead(\"ListUsers\", {}).data;\n  const [name, setName] = useState(\"\");\n  const renameMutate = useWrite(\"RenameUserGroup\", {\n    onSuccess: () => {\n      inv([\"ListUserGroups\"]);\n      toast({ title: \"Renamed User Group\" });\n    },\n  }).mutate;\n  const rename = () => {\n    if (!name) {\n      toast({ title: \"New name cannot be empty\" });\n      if (group) setName(group.name);\n      return;\n    }\n    renameMutate({ id: group_id, name });\n  };\n  const removeMutate = useWrite(\"RemoveUserFromUserGroup\", {\n    onSuccess: () => {\n      inv([\"ListUserGroups\"]);\n      toast({ title: \"Removed User from User Group\" });\n    },\n  }).mutate;\n  const everyoneMutate = useWrite(\"SetEveryoneUserGroup\", {\n    onSuccess: () => {\n      inv([\"ListUserGroups\"]);\n      toast({ title: \"Toggled User Group 'everyone'\" });\n    },\n  }).mutate;\n  if (!group) return null;\n  return (\n    <Page\n      title={group.name}\n      icon={<Users className=\"w-8 h-8\" />}\n      actions={<ExportButton user_groups={[group_id]} />}\n      subtitle={\n        <div className=\"text-sm text-muted-foreground flex gap-2 items-center\">\n          <div>User Group</div>|\n          <div className=\"font-bold\">\n            {group.everyone && \"Everyone\"}\n            {!group.everyone && (group.users ?? []).length > 0 && (\n              <>\n                {(group.users ?? []).length} User\n                {(group.users ?? []).length > 1 ? \"s\" : \"\"}\n              </>\n            )}\n            {!group.everyone && (group.users ?? []).length === 0 && \"No Users\"}\n          </div>\n        </div>\n      }\n    >\n      <Section\n        title=\"Users\"\n        icon={<User className=\"w-4 h-4\" />}\n        titleRight={\n          <div className=\"ml-4 flex gap-4 items-center\">\n            {!group.everyone && <AddUserToGroup group_id={group_id} />}\n            <div className=\"flex gap-2 items-center\">\n              Everyone\n              <Switch\n                checked={group.everyone}\n                onCheckedChange={(everyone) =>\n                  everyoneMutate({ user_group: group_id, everyone })\n                }\n              />\n            </div>\n            {group.everyone && (\n              <div className=\"text-muted-foreground\">\n                All users will inherit the permissions in this group.\n              </div>\n            )}\n          </div>\n        }\n      >\n        {!group.everyone && (\n          <UserTable\n            users={\n              users?.filter((user) =>\n                group ? (group.users ?? []).includes(user._id?.$oid!) : false\n              ) ?? []\n            }\n            onUserRemove={(user_id) =>\n              removeMutate({ user_group: group_id, user: user_id })\n            }\n          />\n        )}\n      </Section>\n\n      <PermissionsTableTabs user_target={{ type: \"UserGroup\", id: group_id }} />\n\n      <div className=\"flex flex-col justify-end w-full gap-4\">\n        <div className=\"flex justify-end w-full\">\n          <div className=\"flex items-center gap-2\">\n            <h2 className=\"text-muted-foreground\">Rename</h2>\n            <Input\n              placeholder=\"Enter new name\"\n              value={name}\n              onChange={(e) => setName(e.target.value)}\n              onKeyDown={(e) => {\n                if (e.key === \"Enter\") rename();\n              }}\n              className=\"w-[300px]\"\n            />\n            <Button variant=\"secondary\" onClick={rename}>\n              <Save className=\"w-4 h-4\" />\n            </Button>\n          </div>\n        </div>\n        <div className=\"flex justify-end w-full\">\n          <DeleteUserGroup group={group} />\n        </div>\n      </div>\n    </Page>\n  );\n}\n\nconst AddUserToGroup = ({ group_id }: { group_id: string }) => {\n  const inv = useInvalidate();\n  const { toast } = useToast();\n\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState(\"\");\n\n  const group = useRead(\"ListUserGroups\", {}).data?.find(\n    (group) => group._id?.$oid === group_id\n  );\n\n  const users = useRead(\"ListUsers\", {}).data?.filter(\n    (user) =>\n      // Only show users not already in group\n      !group?.users?.includes(user._id?.$oid!)\n  );\n\n  const addUser = useWrite(\"AddUserToUserGroup\", {\n    onSuccess: () => {\n      inv([\"ListUserGroups\"]);\n      toast({ title: \"Added User to User Group\" });\n    },\n  }).mutate;\n\n  if (!users || users.length === 0) return null;\n\n  const filtered = filterBySplit(users, search, (item) => item.username);\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"secondary\"\n          className=\"flex justify-start items-center gap-2 w-48 px-3\"\n        >\n          <PlusCircle className=\"w-4 h-4\" />\n          Add User\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        className=\"w-[300px] max-h-[400px] p-0\"\n        sideOffset={12}\n        align=\"start\"\n      >\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Search Users\"\n            className=\"h-9\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            <CommandEmpty className=\"flex justify-evenly items-center\">\n              No Users Found\n              <SearchX className=\"w-3 h-3\" />\n            </CommandEmpty>\n\n            <CommandGroup>\n              {filtered?.map((user) => (\n                <CommandItem\n                  key={user.username}\n                  onSelect={() => {\n                    setOpen(false);\n                    addUser({ user_group: group_id, user: user._id?.$oid! });\n                  }}\n                >\n                  <Button\n                    variant=\"ghost\"\n                    className=\"flex gap-2 items-center p-0\"\n                  >\n                    <UserAvatar avatar={(user.config.data as any).avatar} />\n                    {user.username}\n                  </Button>\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nconst UserAvatar = ({ avatar }: { avatar: string | undefined }) =>\n  avatar ? (\n    <img src={avatar} alt=\"Avatar\" className=\"w-4\" />\n  ) : (\n    <User className=\"w-4\" />\n  );\n"
  },
  {
    "path": "frontend/src/pages/user.tsx",
    "content": "import { KeysTable } from \"@components/keys/table\";\nimport { Page } from \"@components/layouts\";\nimport { PermissionsTableTabs } from \"@components/users/permissions-table\";\nimport {\n  CreateKeyForServiceUser,\n  DeleteKeyForServiceUser,\n} from \"@components/users/service-api-key\";\nimport { ConfirmButton } from \"@components/util\";\nimport { useInvalidate, useRead, useUser, useWrite } from \"@lib/hooks\";\nimport { Label } from \"@ui/label\";\nimport { Switch } from \"@ui/switch\";\nimport { useToast } from \"@ui/use-toast\";\nimport { UserCheck, UserMinus, Users } from \"lucide-react\";\nimport { Link, useParams } from \"react-router-dom\";\nimport { Button } from \"@ui/button\";\nimport { Card, CardContent, CardHeader } from \"@ui/card\";\n\nexport default function UserPage() {\n  const admin_user = useUser().data;\n  const { toast } = useToast();\n  const inv = useInvalidate();\n  const user_id = useParams().id as string;\n  const user = useRead(\"ListUsers\", {}).data?.find(\n    (user) => user._id?.$oid === user_id\n  );\n  const { mutate: update_base } = useWrite(\"UpdateUserBasePermissions\", {\n    onSuccess: () => {\n      inv([\"FindUser\"]);\n      inv([\"ListUsers\"]);\n      toast({ title: \"Modify user base permissions\" });\n    },\n  });\n  const { mutate: update_admin } = useWrite(\"UpdateUserAdmin\", {\n    onSuccess: () => {\n      inv([\"FindUser\"]);\n      inv([\"ListUsers\"]);\n      toast({ title: \"Modify user admin\" });\n    },\n  });\n  const enabledClass = user?.enabled ? \"text-green-500\" : \"text-red-500\";\n  const avatar = (user?.config.data as any)?.avatar as string | undefined;\n  if (!user || !admin_user) return null;\n  return (\n    <Page\n      title={user?.username}\n      icon={avatar && <img src={avatar} alt=\"\" className=\"w-7 h-7\" />}\n      subtitle={\n        <div className=\"text-sm text-muted-foreground flex gap-2\">\n          <div className={enabledClass}>\n            {user?.enabled ? \"Enabled\" : \"Disabled\"}\n          </div>\n          |\n          <div className=\"flex gap-2\">\n            Level:{\" \"}\n            <div className=\"font-bold\">{user?.admin ? \"Admin\" : \"User\"}</div>\n          </div>\n          |\n          <div className=\"flex gap-2\">\n            Type: <div className=\"font-bold\">{user?.config.type}</div>\n          </div>\n        </div>\n      }\n    >\n      {(!user.admin || (!user.super_admin && admin_user.super_admin)) && (\n        <Card>\n          <CardHeader className=\"border-b pb-6\">User Permissions</CardHeader>\n          <CardContent className=\"mt-6 flex gap-8 items-center flex-wrap\">\n            <ConfirmButton\n              title={user.enabled ? \"Disable User\" : \"Enable User\"}\n              icon={\n                user.enabled ? (\n                  <UserMinus className=\"w-4 h-4\" />\n                ) : (\n                  <UserCheck className=\"w-4 h-4\" />\n                )\n              }\n              variant={user.enabled ? \"destructive\" : \"outline\"}\n              onClick={() => update_base({ user_id, enabled: !user.enabled })}\n            />\n            <ConfirmButton\n              title={user.admin ? \"Take Admin\" : \"Make Admin\"}\n              icon={\n                user.admin ? (\n                  <UserMinus className=\"w-4 h-4\" />\n                ) : (\n                  <UserCheck className=\"w-4 h-4\" />\n                )\n              }\n              variant={user.admin ? \"destructive\" : \"outline\"}\n              onClick={() => update_admin({ user_id, admin: !user.admin })}\n            />\n            {user.enabled &&\n              !user.admin &&\n              ([\"Server\", \"Build\"] as Array<\"Server\" | \"Build\">).map((item) => {\n                const key = `create_${item.toLowerCase()}_permissions` as\n                  | \"create_server_permissions\"\n                  | \"create_build_permissions\";\n                const req_key = `create_${item.toLowerCase()}s`;\n                return (\n                  <div\n                    className=\"flex items-center gap-4 cursor-pointer p-2\"\n                    onClick={() =>\n                      update_base({ user_id, [req_key]: !user[key] })\n                    }\n                  >\n                    <Label className=\"cursor-pointer\" htmlFor={key}>\n                      Create {item} Permission\n                    </Label>\n                    <Switch\n                      id={key}\n                      className=\"flex gap-4\"\n                      checked={user[key]}\n                    />\n                  </div>\n                );\n              })}\n          </CardContent>\n        </Card>\n      )}\n      {user.config.type === \"Service\" && <ApiKeysTable user_id={user_id} />}\n      {user.enabled && !user.admin && (\n        <>\n          <Groups user_id={user_id} />\n          <PermissionsTableTabs\n            user_target={{ type: \"User\", id: user._id?.$oid! }}\n          />\n        </>\n      )}\n    </Page>\n  );\n}\n\nconst ApiKeysTable = ({ user_id }: { user_id: string }) => {\n  const keys = useRead(\"ListApiKeysForServiceUser\", { user: user_id }).data;\n  return (\n    <Card>\n      <CardHeader className=\"border-b pb-6 flex flex-row items-center gap-4\">\n        Api Keys <CreateKeyForServiceUser user_id={user_id} />\n      </CardHeader>\n      <CardContent>\n        <KeysTable keys={keys ?? []} DeleteKey={DeleteKeyForServiceUser} />\n      </CardContent>\n    </Card>\n  );\n};\n\nconst Groups = ({ user_id }: { user_id: string }) => {\n  const groups = useRead(\"ListUserGroups\", {}).data?.filter((group) =>\n    group.users?.includes(user_id)\n  );\n  if (!groups || groups.length === 0) {\n    return null;\n  }\n  return (\n    <Card>\n      <CardHeader className=\"border-b pb-6\">Groups</CardHeader>\n      <CardContent className=\"mt-6 grid gap-4 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4\">\n        {groups.map((group) => (\n          <Link to={`/user-groups/${group._id?.$oid}`}>\n            <Button variant=\"link\" className=\"flex gap-2 items-center p-0\">\n              <Users className=\"w-4 h-4\" />\n              {group.name}\n            </Button>\n          </Link>\n        ))}\n      </CardContent>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/user_disabled.tsx",
    "content": "import { LOGIN_TOKENS, useUser } from \"@lib/hooks\";\nimport { Button } from \"@ui/button\";\nimport { UserX } from \"lucide-react\";\n\nexport default function UserDisabled() {\n  const user_id = useUser().data?._id?.$oid;\n  return (\n    <div className=\"w-full h-screen flex justify-center items-center\">\n      <div className=\"flex flex-col gap-4 justify-center items-center\">\n        <UserX className=\"w-16 h-16\" />\n        User Not Enabled\n        <Button\n          variant=\"outline\"\n          onClick={() => {\n            user_id && LOGIN_TOKENS.remove(user_id);\n            location.reload();\n          }}\n        >\n          Log Out\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/router.tsx",
    "content": "import { Layout } from \"@components/layouts\";\nimport { LOGIN_TOKENS, useAuth, useUser } from \"@lib/hooks\";\nimport UpdatePage from \"@pages/update\";\nimport { Loader2 } from \"lucide-react\";\nimport { lazy, Suspense } from \"react\";\nimport {\n  BrowserRouter,\n  Navigate,\n  Outlet,\n  Route,\n  Routes,\n  useLocation,\n} from \"react-router-dom\";\n\n// Lazy import pages\nconst Resources = lazy(() => import(\"@pages/resources\"));\nconst Resource = lazy(() => import(\"@pages/resource\"));\nconst Login = lazy(() => import(\"@pages/login\"));\nconst Tree = lazy(() => import(\"@pages/home/tree\"));\nconst UpdatesPage = lazy(() => import(\"@pages/updates\"));\nconst AllResources = lazy(() => import(\"@pages/home/all_resources\"));\nconst UserDisabled = lazy(() => import(\"@pages/user_disabled\"));\nconst Home = lazy(() => import(\"@pages/home\"));\nconst AlertsPage = lazy(() => import(\"@pages/alerts\"));\nconst UserPage = lazy(() => import(\"@pages/user\"));\nconst UserGroupPage = lazy(() => import(\"@pages/user-group\"));\nconst Settings = lazy(() => import(\"@pages/settings\"));\nconst StackServicePage = lazy(() => import(\"@pages/stack-service\"));\nconst NetworkPage = lazy(() => import(\"@pages/server-info/network\"));\nconst ImagePage = lazy(() => import(\"@pages/server-info/image\"));\nconst VolumePage = lazy(() => import(\"@pages/server-info/volume\"));\nconst ContainerPage = lazy(() => import(\"@pages/server-info/container\"));\nconst ContainersPage = lazy(() => import(\"@pages/containers\"));\nconst SchedulesPage = lazy(() => import(\"@pages/schedules\"));\n\nconst sanitize_query = (search: URLSearchParams) => {\n  search.delete(\"token\");\n  const query = search.toString();\n  location.replace(\n    `${location.origin}${location.pathname}${query.length ? \"?\" + query : \"\"}`\n  );\n};\n\nlet exchange_token_sent = false;\n\n/// returns whether to show login / loading screen depending on state of exchange token loop\nconst useExchangeToken = () => {\n  const search = new URLSearchParams(location.search);\n  const exchange_token = search.get(\"token\");\n  const { mutate } = useAuth(\"ExchangeForJwt\", {\n    onSuccess: ({ user_id, jwt }) => {\n      LOGIN_TOKENS.add_and_change(user_id, jwt);\n      sanitize_query(search);\n    },\n  });\n\n  // In this case, failed to get user (jwt unset / invalid)\n  // and the exchange token is not in url.\n  // Just show the login.\n  if (!exchange_token) return false;\n\n  // guard against multiple reqs sent\n  // maybe isPending would do this but not sure about with render loop, this for sure will.\n  if (!exchange_token_sent) {\n    mutate({ token: exchange_token });\n    exchange_token_sent = true;\n  }\n\n  return true;\n};\n\nexport const Router = () => {\n  // Handle exchange token loop to avoid showing login flash\n  const exchangeTokenPending = useExchangeToken();\n  if (exchangeTokenPending) {\n    return (\n      <div className=\"w-screen h-screen flex justify-center items-center\">\n        <Loader2 className=\"w-8 h-8 animate-spin\" />\n      </div>\n    );\n  }\n\n  return (\n    <Suspense\n      fallback={\n        <div className=\"w-[100vw] h-[100vh] flex items-center justify-center\">\n          <Loader2 className=\"w-16 h-16 animate-spin\" />\n        </div>\n      }\n    >\n      <BrowserRouter>\n        <Routes>\n          <Route path=\"login\" element={<Login />} />\n          <Route element={<RequireAuth />}>\n            <Route path=\"/\" element={<Layout />}>\n              <Route path=\"\" element={<Home />} />\n              <Route path=\"settings\" element={<Settings />} />\n              <Route path=\"tree\" element={<Tree />} />\n              <Route path=\"containers\" element={<ContainersPage />} />\n              <Route path=\"resources\" element={<AllResources />} />\n              <Route path=\"schedules\" element={<SchedulesPage />} />\n              <Route path=\"alerts\" element={<AlertsPage />} />\n              <Route path=\"user-groups/:id\" element={<UserGroupPage />} />\n              <Route path=\"users/:id\" element={<UserPage />} />\n              <Route path=\"updates\">\n                <Route path=\"\" element={<UpdatesPage />} />\n                <Route path=\":id\" element={<UpdatePage />} />\n              </Route>\n              <Route path=\":type\">\n                <Route path=\"\" element={<Resources />} />\n                <Route path=\":id\" element={<Resource />} />\n                <Route\n                  path=\":id/service/:service\"\n                  element={<StackServicePage />}\n                />\n                <Route\n                  path=\":id/container/:container\"\n                  element={<ContainerPage />}\n                />\n                <Route path=\":id/network/:network\" element={<NetworkPage />} />\n                <Route path=\":id/image/:image\" element={<ImagePage />} />\n                <Route path=\":id/volume/:volume\" element={<VolumePage />} />\n              </Route>\n            </Route>\n          </Route>\n        </Routes>\n      </BrowserRouter>\n    </Suspense>\n  );\n\n  // return <RouterProvider router={ROUTER} />;\n};\n\nconst RequireAuth = () => {\n  const { data: user, error } = useUser();\n  const location = useLocation();\n\n  if (!LOGIN_TOKENS.jwt() || error) {\n    if (location.pathname === \"/\") {\n      return <Navigate to=\"/login\" replace />;\n    }\n    const backto = encodeURIComponent(location.pathname + location.search);\n    return <Navigate to={`/login?backto=${backto}`} replace />;\n  }\n\n  if (!user) {\n    return (\n      <div className=\"w-screen h-screen flex justify-center items-center\">\n        <Loader2 className=\"w-8 h-8 animate-spin\" />\n      </div>\n    );\n  }\n\n  if (!user.enabled) return <UserDisabled />;\n\n  return <Outlet />;\n};\n"
  },
  {
    "path": "frontend/src/types.d.ts",
    "content": "import { Types } from \"komodo_client\";\n\nexport type UsableResource = Exclude<Types.ResourceTarget[\"type\"], \"System\">;\n\ntype IdComponent = React.FC<{ id: string }>;\ntype OptionalIdComponent = React.FC<{ id?: string }>;\n\nexport interface RequiredResourceComponents {\n  list_item: (id: string) => Types.ResourceListItem<unknown> | undefined;\n  resource_links: (\n    resource: Types.Resource<unknown, unknown>\n  ) => Array<string> | undefined;\n\n  Description: React.FC;\n\n  /** Header for individual resource pages */\n  ResourcePageHeader: IdComponent;\n\n  /** Summary card for use in dashboard */\n  Dashboard: React.FC;\n\n  /** New resource button / dialog */\n  New: React.FC<{ server_id?: string; build_id?: string }>;\n\n  /** A table component to view resource list */\n  Table: React.FC<{ resources: Types.ResourceListItem<unknown>[] }>;\n\n  /** Dropdown menu to trigger group actions for selected resources */\n  GroupActions: React.FC;\n\n  /** Icon for the component */\n  Icon: OptionalIdComponent;\n  BigIcon: OptionalIdComponent;\n\n  State: IdComponent;\n\n  /** status metrics, like deployment state / status */\n  Status: { [status: string]: IdComponent };\n\n  /**\n   * Some config items shown in header, like deployment server /image\n   * or build repo / branch\n   */\n  Info: { [info: string]: IdComponent };\n\n  /** Action buttons */\n  Actions: { [action: string]: IdComponent };\n\n  /** Resource specific sections */\n  Page: { [section: string]: IdComponent };\n\n  /** Config component for resource */\n  Config: IdComponent;\n\n  /** Danger zone for resource, containing eg rename, delete */\n  DangerZone: IdComponent;\n}\n"
  },
  {
    "path": "frontend/src/ui/badge.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@lib/utils\"\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  )\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "frontend/src/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent-foreground/10 hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2\",\n        sm: \"h-8 rounded-md px-3 text-xs\",\n        lg: \"h-10 rounded-md px-8\",\n        icon: \"h-9 w-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  }\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "frontend/src/ui/card.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-xl border bg-card text-card-foreground shadow\",\n      className\n    )}\n    {...props}\n  />\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col flex-wrap gap-4 space-y-1.5 p-6\", className)}\n    {...props}\n  />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\"font-semibold leading-none tracking-tight\", className)}\n    {...props}\n  />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n))\nCardFooter.displayName = \"CardFooter\"\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }\n"
  },
  {
    "path": "frontend/src/ui/checkbox.tsx",
    "content": "import * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { CheckIcon } from \"@radix-ui/react-icons\"\n\nimport { cn } from \"@lib/utils\"\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <CheckboxPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n      className\n    )}\n    {...props}\n  >\n    <CheckboxPrimitive.Indicator\n      className={cn(\"flex items-center justify-center text-current\")}\n    >\n      <CheckIcon className=\"h-4 w-4\" />\n    </CheckboxPrimitive.Indicator>\n  </CheckboxPrimitive.Root>\n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n"
  },
  {
    "path": "frontend/src/ui/command.tsx",
    "content": "import * as React from \"react\";\nimport { type DialogProps } from \"@radix-ui/react-dialog\";\nimport { MagnifyingGlassIcon } from \"@radix-ui/react-icons\";\nimport { Command as CommandPrimitive } from \"cmdk\";\n\nimport { cn } from \"@lib/utils\";\nimport { Dialog, DialogContent } from \"@//ui/dialog\";\n\nconst Command = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n      className\n    )}\n    {...props}\n  />\n));\nCommand.displayName = CommandPrimitive.displayName;\n\ninterface CommandDialogProps extends DialogProps {\n  manualFilter?: boolean;\n}\n\nconst CommandDialog = ({\n  children,\n  manualFilter,\n  ...props\n}: CommandDialogProps) => {\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"overflow-hidden p-0\">\n        <Command\n          shouldFilter={!manualFilter}\n          className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\"\n        >\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst CommandInput = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Input>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n  <div className=\"flex items-center border-b px-3\" cmdk-input-wrapper=\"\">\n    <MagnifyingGlassIcon className=\"mr-2 h-4 w-4 shrink-0 opacity-50\" />\n    <CommandPrimitive.Input\n      ref={ref}\n      className={cn(\n        \"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    />\n  </div>\n));\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.List\n    ref={ref}\n    className={cn(\"max-h-[300px] overflow-y-auto overflow-x-hidden\", className)}\n    {...props}\n  />\n));\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Empty>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => (\n  <CommandPrimitive.Empty\n    ref={ref}\n    className=\"py-6 text-center text-sm\"\n    {...props}\n  />\n));\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Group>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Group\n    ref={ref}\n    className={cn(\n      \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n      className\n    )}\n    {...props}\n  />\n));\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 h-px bg-border\", className)}\n    {...props}\n  />\n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\nconst CommandItem = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50\",\n      className\n    )}\n    {...props}\n  />\n));\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"ml-auto text-xs tracking-widest text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  );\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n};\n"
  },
  {
    "path": "frontend/src/ui/data-table.tsx",
    "content": "import { cn } from \"@lib/utils\";\nimport {\n  Column,\n  ColumnDef,\n  flexRender,\n  getCoreRowModel,\n  getSortedRowModel,\n  Row,\n  RowSelectionState,\n  SortingState,\n  useReactTable,\n} from \"@tanstack/react-table\";\n\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@ui/table\";\nimport { ArrowDown, ArrowUp, Minus } from \"lucide-react\";\nimport { ReactNode, useEffect, useState } from \"react\";\nimport { Checkbox } from \"./checkbox\";\n\ninterface DataTableProps<TData, TValue> {\n  /** Unique key given to table so sorting can be remembered on local storage */\n  tableKey: string;\n  columns: (ColumnDef<TData, TValue> | false | undefined)[];\n  data: TData[];\n  onRowClick?: (row: TData) => void;\n  noResults?: ReactNode;\n  defaultSort?: SortingState;\n  sortDescFirst?: boolean;\n  selectOptions?: {\n    selectKey: (row: TData) => string;\n    onSelect: (selected: string[]) => void;\n    disableRow?: boolean | ((row: Row<TData>) => boolean);\n  };\n  containerClassName?: string;\n}\n\nexport function DataTable<TData, TValue>({\n  tableKey,\n  columns,\n  data,\n  onRowClick,\n  noResults,\n  sortDescFirst = false,\n  defaultSort = [],\n  selectOptions,\n  containerClassName,\n}: DataTableProps<TData, TValue>) {\n  const [sorting, setSorting] = useState<SortingState>(defaultSort);\n\n  // intentionally not initialized to clear selected values on table mount\n  // could add some prop for adding default selected state to preserve between mounts\n  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});\n\n  const table = useReactTable({\n    data,\n    columns: columns.filter((c) => c) as any,\n    getCoreRowModel: getCoreRowModel(),\n    onSortingChange: setSorting,\n    getSortedRowModel: getSortedRowModel(),\n    state: {\n      sorting,\n      rowSelection,\n    },\n    sortDescFirst,\n    onRowSelectionChange: setRowSelection,\n    getRowId: selectOptions?.selectKey,\n    enableRowSelection: selectOptions?.disableRow,\n  });\n\n  useEffect(() => {\n    const stored = localStorage.getItem(\"data-table-\" + tableKey);\n    const sorting = stored ? (JSON.parse(stored) as SortingState) : null;\n    if (sorting) setSorting(sorting);\n  }, [tableKey]);\n\n  useEffect(() => {\n    localStorage.setItem(\"data-table-\" + tableKey, JSON.stringify(sorting));\n  }, [tableKey, sorting]);\n\n  useEffect(() => {\n    selectOptions?.onSelect(Object.keys(rowSelection));\n  }, [rowSelection]);\n\n  return (\n    <div\n      className={cn(\n        \"rounded-md border bg-card text-card-foreground shadow py-1 px-1\",\n        containerClassName\n      )}\n    >\n      <Table className=\"xl:table-fixed border-separate border-spacing-0\">\n        <TableHeader className=\"sticky top-0 z-40\">\n          {table.getHeaderGroups().map((headerGroup, i) => (\n            <TableRow key={headerGroup.id}>\n              {/* placeholder header */}\n              {i === 0 && selectOptions && (\n                <TableHead className=\"w-8 relative whitespace-nowrap bg-background border-b border-r last:border-r-0\">\n                  <Checkbox\n                    className=\"ml-2\"\n                    disabled={selectOptions.disableRow === true}\n                    checked={\n                      table.getIsSomeRowsSelected()\n                        ? \"indeterminate\"\n                        : table.getIsAllRowsSelected()\n                    }\n                    onCheckedChange={() => table.toggleAllRowsSelected()}\n                  />\n                </TableHead>\n              )}\n              {headerGroup.headers.map((header) => {\n                const size = header.column.getSize();\n                return (\n                  <TableHead\n                    key={header.id}\n                    colSpan={header.colSpan}\n                    className=\"relative whitespace-nowrap bg-background border-b border-r last:border-r-0\"\n                    style={{ width: `${size}px` }}\n                  >\n                    {header.isPlaceholder\n                      ? null\n                      : flexRender(\n                          header.column.columnDef.header,\n                          header.getContext()\n                        )}\n                  </TableHead>\n                );\n              })}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          {table.getRowModel().rows?.length ? (\n            table.getRowModel().rows.map((row) => (\n              <TableRow\n                key={row.id}\n                data-state={row.getIsSelected() && \"selected\"}\n                onClick={() => onRowClick && onRowClick(row.original)}\n                className={cn(\n                  \"even:bg-accent/25\",\n                  onRowClick && \"cursor-pointer\"\n                )}\n              >\n                {selectOptions && (\n                  <TableCell>\n                    <Checkbox\n                      disabled={!row.getCanSelect()}\n                      className=\"ml-2\"\n                      checked={row.getIsSelected()}\n                      onCheckedChange={(c) =>\n                        c !== \"indeterminate\" && row.toggleSelected()\n                      }\n                    />\n                  </TableCell>\n                )}\n                {row.getVisibleCells().map((cell) => {\n                  const size = cell.column.getSize();\n                  return (\n                    <TableCell\n                      key={cell.id}\n                      className=\"p-4 overflow-hidden overflow-ellipsis\"\n                      style={{ width: `${size}px` }}\n                    >\n                      {flexRender(\n                        cell.column.columnDef.cell,\n                        cell.getContext()\n                      )}\n                    </TableCell>\n                  );\n                })}\n              </TableRow>\n            ))\n          ) : (\n            <TableRow>\n              <TableCell colSpan={columns.length} className=\"p-4 text-center\">\n                {noResults ?? \"No results.\"}\n              </TableCell>\n            </TableRow>\n          )}\n        </TableBody>\n      </Table>\n    </div>\n  );\n}\n\nexport const SortableHeader = <T, V>({\n  column,\n  title,\n  sortDescFirst,\n}: {\n  column: Column<T, V>;\n  title: string;\n  sortDescFirst?: boolean;\n}) => (\n  <div\n    className=\"flex items-center justify-between\"\n    onClick={() => column.toggleSorting()}\n  >\n    {title}\n    {column.getIsSorted() === \"asc\" ? (\n      sortDescFirst ? (\n        <ArrowUp className=\"w-4\" />\n      ) : (\n        <ArrowDown className=\"w-4\" />\n      )\n    ) : column.getIsSorted() === \"desc\" ? (\n      sortDescFirst ? (\n        <ArrowDown className=\"w-4\" />\n      ) : (\n        <ArrowUp className=\"w-4\" />\n      )\n    ) : (\n      <Minus className=\"w-4\" />\n    )}\n  </div>\n);\n"
  },
  {
    "path": "frontend/src/ui/dialog.tsx",
    "content": "import * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { Cross2Icon } from \"@radix-ui/react-icons\";\n\nimport { cn } from \"@lib/utils\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ComponentRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ComponentRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <Cross2Icon className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ComponentRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ComponentRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogTrigger,\n  DialogClose,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "frontend/src/ui/dropdown-menu.tsx",
    "content": "import * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport {\n  CheckIcon,\n  ChevronRightIcon,\n  DotFilledIcon,\n} from \"@radix-ui/react-icons\"\n\nimport { cn } from \"@lib/utils\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRightIcon className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md\",\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <CheckIcon className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <DotFilledIcon className=\"h-4 w-4 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  )\n}\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n}\n"
  },
  {
    "path": "frontend/src/ui/hover-card.tsx",
    "content": "import * as React from \"react\"\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\"\n\nimport { cn } from \"@lib/utils\"\n\nconst HoverCard = HoverCardPrimitive.Root\n\nconst HoverCardTrigger = HoverCardPrimitive.Trigger\n\nconst HoverCardContent = React.forwardRef<\n  React.ElementRef<typeof HoverCardPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <HoverCardPrimitive.Content\n    ref={ref}\n    align={align}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nHoverCardContent.displayName = HoverCardPrimitive.Content.displayName\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent }\n"
  },
  {
    "path": "frontend/src/ui/input.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@lib/utils\"\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n"
  },
  {
    "path": "frontend/src/ui/json.tsx",
    "content": "export const Json = ({ json }: any) => {\n  if (!json) {\n    return <p>null</p>;\n  }\n\n  const type = typeof json;\n\n  if (type === \"function\") {\n    return <p>??function??</p>;\n  }\n\n  // null case\n  if (type === \"undefined\") {\n    return <p>null</p>;\n  }\n\n  // base cases\n  if (\n    type === \"bigint\" ||\n    type === \"boolean\" ||\n    type === \"number\" ||\n    type === \"string\" ||\n    type === \"symbol\"\n  ) {\n    return <p>{json}</p>;\n  }\n\n  // Type is object or array\n  if (Array.isArray(json)) {\n    return (\n      <div className=\"flex flex-col gap-2\">\n        {(json as any[]).map((json) => (\n          <Json json={json} />\n        ))}\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      {Object.keys(json).map((key) => (\n        <div className=\"flex gap-2\">\n          <p>{key}</p>: <Json json={json[key]} />\n        </div>\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/ui/label.tsx",
    "content": "import * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@lib/utils\"\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n)\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n))\nLabel.displayName = LabelPrimitive.Root.displayName\n\nexport { Label }\n"
  },
  {
    "path": "frontend/src/ui/multi-select.tsx",
    "content": "import * as React from \"react\";\nimport { Check, ChevronsUpDown, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/ui/badge\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/ui/popover\";\nimport { Skeleton } from \"@/ui/skeleton\";\n\ninterface MultiSelectProps {\n  options?: { label: string; value: string }[];\n  value: string[];\n  onChange: (selected: string[]) => void;\n  placeholder?: string;\n  className?: string;\n  isLoading?: boolean;\n  disabled?: boolean;\n}\n\nfunction MultiSelect({\n  options,\n  value,\n  onChange,\n  placeholder = \"Select items...\",\n  className,\n  isLoading = false,\n  disabled = false,\n}: MultiSelectProps) {\n  const [open, setOpen] = React.useState(false);\n\n  const handleUnselect = (item: string) => {\n    onChange(value.filter((i) => i !== item));\n  };\n\n  const handleSelect = (item: string) => {\n    if (value.includes(item)) {\n      handleUnselect(item);\n    } else {\n      onChange([...value, item]);\n    }\n  };\n\n  return (\n    <div className={cn(\"w-full\", className)}>\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger\n          className={cn(\n            \"flex h-full w-full transition-all items-center justify-between rounded-md border border-input bg-background text-sm\",\n            \"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n            \"disabled:cursor-not-allowed disabled:opacity-50\",\n            \"hover:bg-accent hover:text-accent-foreground\"\n          )}\n          disabled={disabled}\n          aria-expanded={open}\n        >\n          <div className=\"flex justify-between flex-1 overflow-hidden\">\n            <div\n              className=\"flex gap-1 flex-1 py-2 px-3 overflow-x-auto\"\n              style={{\n                scrollbarWidth: \"thin\",\n                scrollbarColor: \"hsl(var(--border)) transparent\",\n              }}\n            >\n              {value.length === 0 ? (\n                <span className=\"text-muted-foreground truncate\">\n                  {placeholder}\n                </span>\n              ) : (\n                value.map((item) => {\n                  const option = options?.find((opt) => opt.value === item);\n                  return (\n                    <Badge key={item} variant=\"default\" className=\"text-xs\">\n                      {option?.label}\n                      <span\n                        role=\"button\"\n                        tabIndex={0}\n                        className=\"ml-1 hover:bg-destructive transition-all hover:text-destructive-foreground rounded-full p-0.5\"\n                        onKeyDown={(e) =>\n                          e.key === \"Enter\" && handleUnselect(item)\n                        }\n                        onClick={() => handleUnselect(item)}\n                      >\n                        <X className=\"h-3 w-3\" />\n                      </span>\n                    </Badge>\n                  );\n                })\n              )}\n            </div>\n            <hr className=\"border-l border-border bg-red-300 h-6 mx-0.5 my-auto\" />\n            <span\n              role=\"button\"\n              onClick={(e) => {\n                e.stopPropagation();\n                setOpen((prev) => !prev);\n              }}\n              tabIndex={0}\n              className={cn(\n                \"p-1 mx-1.5 my-auto h-full outline-none\",\n                \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n                \"hover:bg-accent/50 rounded-sm cursor-pointer\"\n              )}\n            >\n              <ChevronsUpDown className=\"h-4 w-4 shrink-0 opacity-50\" />\n            </span>\n          </div>\n        </PopoverTrigger>\n        <PopoverContent className=\"w-full p-0\" align=\"start\">\n          <Command>\n            <CommandInput autoFocus={false} placeholder=\"Search items...\" />\n            <CommandList>\n              <CommandEmpty className=\"p-0\">\n                {isLoading ? (\n                  <div className=\"p-2\">\n                    {Array.from({ length: 6 }).map((_, index) => (\n                      <Skeleton\n                        key={index}\n                        className=\"h-4 w-full mb-1 last:mb-0\"\n                      />\n                    ))}\n                  </div>\n                ) : (\n                  <div className=\"text-center text-sm py-4 text-muted-foreground\">\n                    No items found.\n                  </div>\n                )}\n              </CommandEmpty>\n              <CommandGroup>\n                {options?.map((option) => (\n                  <CommandItem\n                    key={option.value}\n                    value={option.value}\n                    onSelect={() => handleSelect(option.value)}\n                  >\n                    <Check\n                      className={cn(\n                        \"mr-2 h-4 w-4\",\n                        value.includes(option.value)\n                          ? \"opacity-100\"\n                          : \"opacity-0\"\n                      )}\n                    />\n                    {option.label}\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n            </CommandList>\n          </Command>\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n\nexport {MultiSelect}\n"
  },
  {
    "path": "frontend/src/ui/pagination.tsx",
    "content": "import * as React from \"react\"\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  DotsHorizontalIcon,\n} from \"@radix-ui/react-icons\"\n\nimport { cn } from \"@lib/utils\"\nimport { ButtonProps, buttonVariants } from \"@//ui/button\"\n\nconst Pagination = ({ className, ...props }: React.ComponentProps<\"nav\">) => (\n  <nav\n    role=\"navigation\"\n    aria-label=\"pagination\"\n    className={cn(\"mx-auto flex w-full justify-center\", className)}\n    {...props}\n  />\n)\nPagination.displayName = \"Pagination\"\n\nconst PaginationContent = React.forwardRef<\n  HTMLUListElement,\n  React.ComponentProps<\"ul\">\n>(({ className, ...props }, ref) => (\n  <ul\n    ref={ref}\n    className={cn(\"flex flex-row items-center gap-1\", className)}\n    {...props}\n  />\n))\nPaginationContent.displayName = \"PaginationContent\"\n\nconst PaginationItem = React.forwardRef<\n  HTMLLIElement,\n  React.ComponentProps<\"li\">\n>(({ className, ...props }, ref) => (\n  <li ref={ref} className={cn(\"\", className)} {...props} />\n))\nPaginationItem.displayName = \"PaginationItem\"\n\ntype PaginationLinkProps = {\n  isActive?: boolean\n} & Pick<ButtonProps, \"size\"> &\n  React.ComponentProps<\"a\">\n\nconst PaginationLink = ({\n  className,\n  isActive,\n  size = \"icon\",\n  ...props\n}: PaginationLinkProps) => (\n  <a\n    aria-current={isActive ? \"page\" : undefined}\n    className={cn(\n      buttonVariants({\n        variant: isActive ? \"outline\" : \"ghost\",\n        size,\n      }),\n      className\n    )}\n    {...props}\n  />\n)\nPaginationLink.displayName = \"PaginationLink\"\n\nconst PaginationPrevious = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to previous page\"\n    size=\"default\"\n    className={cn(\"gap-1 pl-2.5\", className)}\n    {...props}\n  >\n    <ChevronLeftIcon className=\"h-4 w-4\" />\n    <span>Previous</span>\n  </PaginationLink>\n)\nPaginationPrevious.displayName = \"PaginationPrevious\"\n\nconst PaginationNext = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to next page\"\n    size=\"default\"\n    className={cn(\"gap-1 pr-2.5\", className)}\n    {...props}\n  >\n    <span>Next</span>\n    <ChevronRightIcon className=\"h-4 w-4\" />\n  </PaginationLink>\n)\nPaginationNext.displayName = \"PaginationNext\"\n\nconst PaginationEllipsis = ({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) => (\n  <span\n    aria-hidden\n    className={cn(\"flex h-9 w-9 items-center justify-center\", className)}\n    {...props}\n  >\n    <DotsHorizontalIcon className=\"h-4 w-4\" />\n    <span className=\"sr-only\">More pages</span>\n  </span>\n)\nPaginationEllipsis.displayName = \"PaginationEllipsis\"\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationLink,\n  PaginationItem,\n  PaginationPrevious,\n  PaginationNext,\n  PaginationEllipsis,\n}\n"
  },
  {
    "path": "frontend/src/ui/popover.tsx",
    "content": "import * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"@lib/utils\"\n\nconst Popover = PopoverPrimitive.Root\n\nconst PopoverTrigger = PopoverPrimitive.Trigger\n\nconst PopoverAnchor = PopoverPrimitive.Anchor\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n))\nPopoverContent.displayName = PopoverPrimitive.Content.displayName\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }\n"
  },
  {
    "path": "frontend/src/ui/progress.tsx",
    "content": "import * as React from \"react\"\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\"\n\nimport { cn } from \"@lib/utils\"\n\nconst Progress = React.forwardRef<\n  React.ElementRef<typeof ProgressPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>\n>(({ className, value, ...props }, ref) => (\n  <ProgressPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative h-2 w-full overflow-hidden rounded-full bg-primary/20\",\n      className\n    )}\n    {...props}\n  >\n    <ProgressPrimitive.Indicator\n      className=\"h-full w-full flex-1 bg-primary transition-all\"\n      style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n    />\n  </ProgressPrimitive.Root>\n))\nProgress.displayName = ProgressPrimitive.Root.displayName\n\nexport { Progress }\n"
  },
  {
    "path": "frontend/src/ui/select.tsx",
    "content": "import * as React from \"react\"\nimport {\n  CaretSortIcon,\n  CheckIcon,\n  ChevronDownIcon,\n  ChevronUpIcon,\n} from \"@radix-ui/react-icons\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\n\nimport { cn } from \"@lib/utils\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <CaretSortIcon className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n))\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronUpIcon />\n  </SelectPrimitive.ScrollUpButton>\n))\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronDownIcon />\n  </SelectPrimitive.ScrollDownButton>\n))\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"px-2 py-1.5 text-sm font-semibold\", className)}\n    {...props}\n  />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <CheckIcon className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n))\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n}\n"
  },
  {
    "path": "frontend/src/ui/separator.tsx",
    "content": "import * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"@lib/utils\"\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = \"horizontal\", decorative = true, ...props },\n    ref\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border\",\n        orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n        className\n      )}\n      {...props}\n    />\n  )\n)\nSeparator.displayName = SeparatorPrimitive.Root.displayName\n\nexport { Separator }\n"
  },
  {
    "path": "frontend/src/ui/sheet.tsx",
    "content": "import * as React from \"react\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { Cross2Icon } from \"@radix-ui/react-icons\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@lib/utils\";\n\nconst Sheet = SheetPrimitive.Root;\n\nconst SheetTrigger = SheetPrimitive.Trigger;\n\nconst SheetClose = SheetPrimitive.Close;\n\nconst SheetPortal = SheetPrimitive.Portal;\n\nconst SheetOverlay = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nSheetOverlay.displayName = SheetPrimitive.Overlay.displayName;\n\nconst sheetVariants = cva(\n  \"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-200 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out\",\n  {\n    variants: {\n      side: {\n        top: \"inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top\",\n        bottom:\n          \"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom\",\n        left: \"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm\",\n        right:\n          \"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm\",\n      },\n    },\n    defaultVariants: {\n      side: \"right\",\n    },\n  }\n);\n\ninterface SheetContentProps\n  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,\n    VariantProps<typeof sheetVariants> {}\n\nconst SheetContent = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Content>,\n  SheetContentProps\n>(({ side = \"right\", className, children, ...props }, ref) => (\n  <SheetPortal>\n    <SheetOverlay />\n    <SheetPrimitive.Content\n      ref={ref}\n      className={cn(sheetVariants({ side }), className)}\n      {...props}\n    >\n      <SheetPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary\">\n        <Cross2Icon className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </SheetPrimitive.Close>\n      {children}\n    </SheetPrimitive.Content>\n  </SheetPortal>\n));\nSheetContent.displayName = SheetPrimitive.Content.displayName;\n\nconst SheetHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-2 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n);\nSheetHeader.displayName = \"SheetHeader\";\n\nconst SheetFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n);\nSheetFooter.displayName = \"SheetFooter\";\n\nconst SheetTitle = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold text-foreground\", className)}\n    {...props}\n  />\n));\nSheetTitle.displayName = SheetPrimitive.Title.displayName;\n\nconst SheetDescription = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nSheetDescription.displayName = SheetPrimitive.Description.displayName;\n\nexport {\n  Sheet,\n  SheetPortal,\n  SheetOverlay,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n};\n"
  },
  {
    "path": "frontend/src/ui/skeleton.tsx",
    "content": "import { cn } from \"@lib/utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-primary/10\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Skeleton }\n"
  },
  {
    "path": "frontend/src/ui/switch.tsx",
    "content": "import * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } from \"@lib/utils\"\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n      className\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0\"\n      )}\n    />\n  </SwitchPrimitives.Root>\n))\nSwitch.displayName = SwitchPrimitives.Root.displayName\n\nexport { Switch }\n"
  },
  {
    "path": "frontend/src/ui/table.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@lib/utils\";\n\nconst Table = React.forwardRef<\n  HTMLTableElement,\n  React.HTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n  <div className=\"relative w-full max-h-[60vh] overflow-auto\">\n    <table\n      ref={ref}\n      className={cn(\"w-full caption-bottom text-sm\", className)}\n      {...props}\n    />\n  </div>\n));\nTable.displayName = \"Table\";\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead ref={ref} className={cn(\"[&_tr]:border-b\", className)} {...props} />\n));\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn(\"[&_tr:last-child]:border-0\", className)}\n    {...props}\n  />\n));\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn(\n      \"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0\",\n      className\n    )}\n    {...props}\n  />\n));\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n  <tr\n    ref={ref}\n    className={cn(\n      \"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted\",\n      className\n    )}\n    {...props}\n  />\n));\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      \"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n      className\n    )}\n    {...props}\n  />\n));\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn(\n      \"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n      className\n    )}\n    {...props}\n  />\n));\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption\n    ref={ref}\n    className={cn(\"mt-4 text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nTableCaption.displayName = \"TableCaption\";\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "frontend/src/ui/tabs.tsx",
    "content": "import * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"@lib/utils\"\n\nconst Tabs = TabsPrimitive.Root\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      \"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsList.displayName = TabsPrimitive.List.displayName\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n"
  },
  {
    "path": "frontend/src/ui/textarea.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@lib/utils\"\n\nexport interface TextareaProps\n  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n"
  },
  {
    "path": "frontend/src/ui/theme.tsx",
    "content": "import { createContext, useContext, useEffect, useState } from \"react\";\nimport { Button } from \"@ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@ui/dropdown-menu\";\nimport { CheckCircle, Moon, Sun } from \"lucide-react\";\n\ntype Theme = \"dark\" | \"light\" | \"system\";\n\ntype ThemeProviderProps = {\n  children: React.ReactNode;\n  defaultTheme?: Theme;\n  storageKey?: string;\n};\n\ntype ThemeProviderState = {\n  theme: Theme;\n  currentTheme: Exclude<Theme, \"system\">;\n  setTheme: (theme: Theme) => void;\n};\n\nconst initialState: ThemeProviderState = {\n  theme: \"system\",\n  currentTheme: \"dark\",\n  setTheme: () => null,\n};\n\nconst ThemeProviderContext = createContext<ThemeProviderState>(initialState);\n\nconst systemTheme = () =>\n  window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n\nexport function ThemeProvider({\n  children,\n  defaultTheme = \"system\",\n  storageKey = \"vite-ui-theme\",\n  ...props\n}: ThemeProviderProps) {\n  const [theme, setTheme] = useState<Theme>(\n    () => (localStorage.getItem(storageKey) as Theme) || defaultTheme\n  );\n  // Tracks the current theme\n  //   - if theme is light or dark, equal to theme.\n  //   - if theme is system, tracks current theme with pool loop\n  const [currentTheme, setCurrentTheme] = useState<Exclude<Theme, \"system\">>(\n    theme === \"system\" ? systemTheme() : theme\n  );\n\n  useEffect(() => {\n    if (theme === \"system\") {\n      setCurrentTheme(systemTheme());\n      // For 'system' theme, need to poll\n      // matchMedia for update to theme.\n      const interval = setInterval(() => {\n        setCurrentTheme(systemTheme());\n      }, 5_000);\n      return () => clearInterval(interval);\n    } else {\n      setCurrentTheme(theme);\n    }\n  }, [theme]);\n\n  useEffect(() => {\n    const root = window.document.documentElement;\n    root.classList.add(currentTheme);\n    return () => root.classList.remove(currentTheme);\n  }, [currentTheme]);\n\n  const value = {\n    theme,\n    currentTheme,\n    setTheme: (theme: Theme) => {\n      localStorage.setItem(storageKey, theme);\n      setTheme(theme);\n    },\n  };\n\n  return (\n    <ThemeProviderContext.Provider {...props} value={value}>\n      {children}\n    </ThemeProviderContext.Provider>\n  );\n}\n\nexport const useTheme = () => {\n  const context = useContext(ThemeProviderContext);\n\n  if (context === undefined)\n    throw new Error(\"useTheme must be used within a ThemeProvider\");\n\n  return context;\n};\n\nexport function ThemeToggle() {\n  const { theme, setTheme } = useTheme();\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\">\n          <Sun className=\"w-4 h-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0\" />\n          <Moon className=\"absolute w-4 h-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100\" />\n          <span className=\"sr-only\">Toggle theme</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" sideOffset={20}>\n        <DropdownMenuItem\n          className=\"cursor-pointer flex items-center justify-between\"\n          onClick={() => setTheme(\"light\")}\n        >\n          Light\n          {theme === \"light\" && <CheckCircle className=\"w-3 h-3\" />}\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          className=\"cursor-pointer flex items-center justify-between\"\n          onClick={() => setTheme(\"dark\")}\n        >\n          Dark\n          {theme === \"dark\" && <CheckCircle className=\"w-3 h-3\" />}\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          className=\"cursor-pointer flex items-center justify-between\"\n          onClick={() => setTheme(\"system\")}\n        >\n          System\n          {theme === \"system\" && <CheckCircle className=\"w-3 h-3\" />}\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "frontend/src/ui/toast.tsx",
    "content": "import * as React from \"react\";\nimport { Cross2Icon } from \"@radix-ui/react-icons\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@lib/utils\";\n\nconst ToastProvider = ToastPrimitives.Provider;\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px] gap-2\",\n      className\n    )}\n    {...props}\n  />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n  {\n    variants: {\n      variant: {\n        default: \"border bg-background text-foreground\",\n        destructive:\n          \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n);\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  );\n});\nToast.displayName = ToastPrimitives.Root.displayName;\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive\",\n      className\n    )}\n    {...props}\n  />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      \"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n      className\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <Cross2Icon className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn(\"text-sm font-semibold [&+div]:text-xs\", className)}\n    {...props}\n  />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn(\"text-sm opacity-90\", className)}\n    {...props}\n  />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>;\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n};\n"
  },
  {
    "path": "frontend/src/ui/toaster.tsx",
    "content": "import {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from \"@//ui/toast\"\nimport { useToast } from \"@//ui/use-toast\"\n\nexport function Toaster() {\n  const { toasts } = useToast()\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription>{description}</ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        )\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  )\n}\n"
  },
  {
    "path": "frontend/src/ui/toggle-group.tsx",
    "content": "import * as React from \"react\"\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@lib/utils\"\nimport { toggleVariants } from \"@//ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n  VariantProps<typeof toggleVariants>\n>({\n  size: \"default\",\n  variant: \"default\",\n})\n\nconst ToggleGroup = React.forwardRef<\n  React.ElementRef<typeof ToggleGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &\n    VariantProps<typeof toggleVariants>\n>(({ className, variant, size, children, ...props }, ref) => (\n  <ToggleGroupPrimitive.Root\n    ref={ref}\n    className={cn(\"flex items-center justify-center gap-1\", className)}\n    {...props}\n  >\n    <ToggleGroupContext.Provider value={{ variant, size }}>\n      {children}\n    </ToggleGroupContext.Provider>\n  </ToggleGroupPrimitive.Root>\n))\n\nToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName\n\nconst ToggleGroupItem = React.forwardRef<\n  React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &\n    VariantProps<typeof toggleVariants>\n>(({ className, children, variant, size, ...props }, ref) => {\n  const context = React.useContext(ToggleGroupContext)\n\n  return (\n    <ToggleGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        toggleVariants({\n          variant: context.variant || variant,\n          size: context.size || size,\n        }),\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </ToggleGroupPrimitive.Item>\n  )\n})\n\nToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName\n\nexport { ToggleGroup, ToggleGroupItem }\n"
  },
  {
    "path": "frontend/src/ui/toggle.tsx",
    "content": "import * as React from \"react\"\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@lib/utils\"\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-9 px-3\",\n        sm: \"h-8 px-2\",\n        lg: \"h-10 px-3\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nconst Toggle = React.forwardRef<\n  React.ElementRef<typeof TogglePrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &\n    VariantProps<typeof toggleVariants>\n>(({ className, variant, size, ...props }, ref) => (\n  <TogglePrimitive.Root\n    ref={ref}\n    className={cn(toggleVariants({ variant, size, className }))}\n    {...props}\n  />\n))\n\nToggle.displayName = TogglePrimitive.Root.displayName\n\nexport { Toggle, toggleVariants }\n"
  },
  {
    "path": "frontend/src/ui/tooltip.tsx",
    "content": "import * as React from \"react\";\nimport {\n  useFloating,\n  autoUpdate,\n  offset,\n  flip,\n  shift,\n  useHover,\n  useFocus,\n  useDismiss,\n  useRole,\n  useInteractions,\n  useMergeRefs,\n  FloatingPortal,\n  Placement,\n} from \"@floating-ui/react\";\nimport { cn } from \"@lib/utils\";\n\ninterface TooltipOptions {\n  initialOpen?: boolean;\n  placement?: Placement;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nexport function useTooltip({\n  initialOpen = false,\n  placement = \"bottom-start\",\n  open: controlledOpen,\n  onOpenChange: setControlledOpen,\n}: TooltipOptions = {}) {\n  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);\n\n  const open = controlledOpen ?? uncontrolledOpen;\n  const setOpen = setControlledOpen ?? setUncontrolledOpen;\n\n  const data = useFloating({\n    placement,\n    open,\n    onOpenChange: setOpen,\n    whileElementsMounted: autoUpdate,\n    middleware: [\n      offset(5),\n      flip({\n        crossAxis: placement.includes(\"-\"),\n        fallbackAxisSideDirection: \"start\",\n        padding: 5,\n      }),\n      shift({ padding: 5 }),\n    ],\n  });\n\n  const context = data.context;\n\n  const hover = useHover(context, {\n    move: false,\n    enabled: controlledOpen == null,\n  });\n  const focus = useFocus(context, {\n    enabled: controlledOpen == null,\n  });\n  const dismiss = useDismiss(context);\n  const role = useRole(context, { role: \"tooltip\" });\n\n  const interactions = useInteractions([hover, focus, dismiss, role]);\n\n  return React.useMemo(\n    () => ({\n      open,\n      setOpen,\n      ...interactions,\n      ...data,\n    }),\n    [open, setOpen, interactions, data]\n  );\n}\n\ntype ContextType = ReturnType<typeof useTooltip> | null;\n\nconst TooltipContext = React.createContext<ContextType>(null);\n\nexport const useTooltipContext = () => {\n  const context = React.useContext(TooltipContext);\n\n  if (context == null) {\n    throw new Error(\"Tooltip components must be wrapped in <Tooltip />\");\n  }\n\n  return context;\n};\n\nexport function Tooltip({\n  children,\n  ...options\n}: { children: React.ReactNode } & TooltipOptions) {\n  // This can accept any props as options, e.g. `placement`,\n  // or other positioning options.\n  const tooltip = useTooltip(options);\n  return (\n    <TooltipContext.Provider value={tooltip}>\n      {children}\n    </TooltipContext.Provider>\n  );\n}\n\nexport const TooltipTrigger = React.forwardRef<\n  HTMLElement,\n  React.HTMLProps<HTMLElement> & { asChild?: boolean }\n>(({ children, asChild = false, ...props }, propRef) => {\n  const context = useTooltipContext();\n  const childrenRef = (children as any).ref;\n  const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);\n\n  // `asChild` allows the user to pass any element as the anchor\n  if (asChild && React.isValidElement(children)) {\n    return React.cloneElement(\n      children,\n      context.getReferenceProps({\n        ref,\n        ...props,\n        ...(children.props as any),\n        \"data-state\": context.open ? \"open\" : \"closed\",\n      })\n    );\n  }\n\n  return (\n    <button\n      ref={ref}\n      // The user can style the trigger based on the state\n      data-state={context.open ? \"open\" : \"closed\"}\n      {...context.getReferenceProps(props)}\n      type=\"button\"\n    >\n      {children}\n    </button>\n  );\n});\nTooltipTrigger.displayName = \"TooltipTrigger\";\n\nexport const TooltipContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLProps<HTMLDivElement>\n>(({ className, ...props }, propRef) => {\n  const context = useTooltipContext();\n  const ref = useMergeRefs([context.refs.setFloating, propRef]);\n\n  if (!context.open) return null;\n\n  return (\n    <FloatingPortal>\n      <div\n        ref={ref}\n        style={{\n          ...context.floatingStyles,\n        }}\n        {...context.getFloatingProps(props)}\n        className={cn(\n          \"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none\",\n          className\n        )}\n      />\n    </FloatingPortal>\n  );\n});\nTooltipContent.displayName = \"TooltipContent\";\n"
  },
  {
    "path": "frontend/src/ui/use-toast.ts",
    "content": "// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type {\n  ToastActionElement,\n  ToastProps,\n} from \"@//ui/toast\"\n\nconst TOAST_LIMIT = 1\nconst TOAST_REMOVE_DELAY = 1000000\n\ntype ToasterToast = ToastProps & {\n  id: string\n  title?: React.ReactNode\n  description?: React.ReactNode\n  action?: ToastActionElement\n}\n\nconst actionTypes = {\n  ADD_TOAST: \"ADD_TOAST\",\n  UPDATE_TOAST: \"UPDATE_TOAST\",\n  DISMISS_TOAST: \"DISMISS_TOAST\",\n  REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const\n\nlet count = 0\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER\n  return count.toString()\n}\n\ntype ActionType = typeof actionTypes\n\ntype Action =\n  | {\n      type: ActionType[\"ADD_TOAST\"]\n      toast: ToasterToast\n    }\n  | {\n      type: ActionType[\"UPDATE_TOAST\"]\n      toast: Partial<ToasterToast>\n    }\n  | {\n      type: ActionType[\"DISMISS_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n  | {\n      type: ActionType[\"REMOVE_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n\ninterface State {\n  toasts: ToasterToast[]\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId)\n    dispatch({\n      type: \"REMOVE_TOAST\",\n      toastId: toastId,\n    })\n  }, TOAST_REMOVE_DELAY)\n\n  toastTimeouts.set(toastId, timeout)\n}\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case \"ADD_TOAST\":\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      }\n\n    case \"UPDATE_TOAST\":\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t\n        ),\n      }\n\n    case \"DISMISS_TOAST\": {\n      const { toastId } = action\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId)\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id)\n        })\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t\n        ),\n      }\n    }\n    case \"REMOVE_TOAST\":\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        }\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      }\n  }\n}\n\nconst listeners: Array<(state: State) => void> = []\n\nlet memoryState: State = { toasts: [] }\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action)\n  listeners.forEach((listener) => {\n    listener(memoryState)\n  })\n}\n\ntype Toast = Omit<ToasterToast, \"id\">\n\nfunction toast({ ...props }: Toast) {\n  const id = genId()\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: \"UPDATE_TOAST\",\n      toast: { ...props, id },\n    })\n  const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id })\n\n  dispatch({\n    type: \"ADD_TOAST\",\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss()\n      },\n    },\n  })\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  }\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState)\n\n  React.useEffect(() => {\n    listeners.push(setState)\n    return () => {\n      const index = listeners.indexOf(setState)\n      if (index > -1) {\n        listeners.splice(index, 1)\n      }\n    }\n  }, [state])\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n  }\n}\n\nexport { useToast, toast }\n"
  },
  {
    "path": "frontend/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "frontend/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: [\"class\"],\n  content: [\n    \"./pages/**/*.{ts,tsx}\",\n    \"./components/**/*.{ts,tsx}\",\n    \"./app/**/*.{ts,tsx}\",\n    \"./src/**/*.{ts,tsx}\",\n  ],\n  safelist: [\n    // General UI colors\n    // red\n    \"text-red-400\",\n    \"text-red-500\",\n    \"text-red-600\",\n    \"text-red-700\",\n    \"bg-red-400\",\n    \"bg-red-500\",\n    \"bg-red-600\",\n    \"bg-red-700\",\n    \"fill-red-400\",\n    \"fill-red-500\",\n    \"fill-red-600\",\n    \"fill-red-700\",\n    \"stroke-red-400\",\n    \"stroke-red-500\",\n    \"stroke-red-600\",\n    \"stroke-red-700\",\n    // green\n    \"text-green-400\",\n    \"text-green-500\",\n    \"text-green-600\",\n    \"text-green-700\",\n    \"bg-green-400\",\n    \"bg-green-500\",\n    \"bg-green-600\",\n    \"bg-green-700\",\n    \"fill-green-400\",\n    \"fill-green-500\",\n    \"fill-green-600\",\n    \"fill-green-700\",\n    \"stroke-green-400\",\n    \"stroke-green-500\",\n    \"stroke-green-600\",\n    \"stroke-green-700\",\n    // blue\n    \"text-blue-400\",\n    \"text-blue-500\",\n    \"text-blue-600\",\n    \"text-blue-700\",\n    \"bg-blue-400\",\n    \"bg-blue-500\",\n    \"bg-blue-600\",\n    \"bg-blue-700\",\n    \"fill-blue-400\",\n    \"fill-blue-500\",\n    \"fill-blue-600\",\n    \"fill-blue-700\",\n    \"stroke-blue-400\",\n    \"stroke-blue-500\",\n    \"stroke-blue-600\",\n    \"stroke-blue-700\",\n    // orange\n    \"text-orange-400\",\n    \"text-orange-500\",\n    \"text-orange-600\",\n    \"text-orange-700\",\n    \"bg-orange-400\",\n    \"bg-orange-500\",\n    \"bg-orange-600\",\n    \"bg-orange-700\",\n    \"fill-orange-400\",\n    \"fill-orange-500\",\n    \"fill-orange-600\",\n    \"fill-orange-700\",\n    \"stroke-orange-400\",\n    \"stroke-orange-500\",\n    \"stroke-orange-600\",\n    \"stroke-orange-700\",\n    // purple\n    \"text-purple-400\",\n    \"text-purple-500\",\n    \"text-purple-600\",\n    \"text-purple-700\",\n    \"bg-purple-400\",\n    \"bg-purple-500\",\n    \"bg-purple-600\",\n    \"bg-purple-700\",\n    \"fill-purple-400\",\n    \"fill-purple-500\",\n    \"fill-purple-600\",\n    \"fill-purple-700\",\n    \"stroke-purple-400\",\n    \"stroke-purple-500\",\n    \"stroke-purple-600\",\n    \"stroke-purple-700\",\n\n    // Tag colors\n    \"bg-slate-400\",\n    \"bg-slate-600\",\n    \"bg-slate-900\",\n    //\n    \"bg-gray-400\",\n    \"bg-gray-600\",\n    \"bg-gray-900\",\n    //\n    \"bg-zinc-400\",\n    \"bg-zinc-600\",\n    \"bg-zinc-900\",\n    //\n    \"bg-neutral-400\",\n    \"bg-neutral-600\",\n    \"bg-neutral-900\",\n    //\n    \"bg-stone-400\",\n    \"bg-stone-600\",\n    \"bg-stone-900\",\n    //\n    \"bg-red-400\",\n    \"bg-red-600\",\n    \"bg-red-900\",\n    //\n    \"bg-orange-400\",\n    \"bg-orange-600\",\n    \"bg-orange-900\",\n    //\n    \"bg-amber-400\",\n    \"bg-amber-600\",\n    \"bg-amber-900\",\n    //\n    \"bg-yellow-400\",\n    \"bg-yellow-600\",\n    \"bg-yellow-900\",\n    //\n    \"bg-lime-400\",\n    \"bg-lime-600\",\n    \"bg-lime-900\",\n    //\n    \"bg-green-400\",\n    \"bg-green-600\",\n    \"bg-green-900\",\n    //\n    \"bg-emerald-400\",\n    \"bg-emerald-600\",\n    \"bg-emerald-900\",\n    //\n    \"bg-teal-400\",\n    \"bg-teal-600\",\n    \"bg-teal-900\",\n    //\n    \"bg-cyan-400\",\n    \"bg-cyan-600\",\n    \"bg-cyan-900\",\n    //\n    \"bg-sky-400\",\n    \"bg-sky-600\",\n    \"bg-sky-900\",\n    //\n    \"bg-blue-400\",\n    \"bg-blue-600\",\n    \"bg-blue-900\",\n    //\n    \"bg-indigo-400\",\n    \"bg-indigo-600\",\n    \"bg-indigo-900\",\n    //\n    \"bg-violet-400\",\n    \"bg-violet-600\",\n    \"bg-violet-900\",\n    //\n    \"bg-purple-400\",\n    \"bg-purple-600\",\n    \"bg-purple-900\",\n    //\n    \"bg-fuchsia-400\",\n    \"bg-fuchsia-600\",\n    \"bg-fuchsia-900\",\n    //\n    \"bg-pink-400\",\n    \"bg-pink-600\",\n    \"bg-pink-900\",\n    //\n    \"bg-rose-400\",\n    \"bg-rose-600\",\n    \"bg-rose-900\",\n  ],\n  prefix: \"\",\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1680px\",\n      },\n    },\n    extend: {\n      colors: {\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: \"0\" },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: \"0\" },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n      },\n    },\n  },\n  plugins: [require(\"tailwindcss-animate\")],\n};\n"
  },
  {
    "path": "frontend/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\n      \"ESNext\",\n      \"DOM\",\n      \"DOM.Iterable\"\n    ],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noImplicitAny\": false,\n    /* Paths */\n    \"baseUrl\": \"./src\",\n    \"paths\": {\n      \"@*\": [\n        \"./*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"src\"\n  ],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    }\n  ]\n}"
  },
  {
    "path": "frontend/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "frontend/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport tspaths from \"vite-tsconfig-paths\";\nimport react from \"@vitejs/plugin-react\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [react(), tspaths()],\n});\n"
  },
  {
    "path": "komodo.code-workspace",
    "content": "{\n\t\"folders\": [\n\t\t{\n\t\t\t\"path\": \".\"\n\t\t}\n\t]\n}"
  },
  {
    "path": "lib/cache/Cargo.toml",
    "content": "[package]\nname = \"cache\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[dependencies]\nanyhow.workspace = true\ntokio.workspace = true"
  },
  {
    "path": "lib/cache/README.md",
    "content": "# Cache module\n\nContains a thread-safe async timeout cache implementation.\nUsed to cache outputs in memory for a limited time period.\nCan be used to avoid re-running the underlying process which generated an output in too short a timeframe."
  },
  {
    "path": "lib/cache/src/lib.rs",
    "content": "use std::{collections::HashMap, hash::Hash, sync::Arc};\n\nuse tokio::sync::Mutex;\n\n/// Prevents simultaneous / rapid fire access to an action,\n/// returning the cached result instead in these situations.\n#[derive(Default)]\npub struct TimeoutCache<K, Res>(\n  Mutex<HashMap<K, Arc<Mutex<CacheEntry<Res>>>>>,\n);\n\nimpl<K: Eq + Hash, Res: Default> TimeoutCache<K, Res> {\n  pub async fn get_lock(\n    &self,\n    key: K,\n  ) -> Arc<Mutex<CacheEntry<Res>>> {\n    let mut lock = self.0.lock().await;\n    lock.entry(key).or_default().clone()\n  }\n}\n\npub struct CacheEntry<Res> {\n  /// The last cached ts\n  pub last_ts: i64,\n  /// The last cached result\n  pub res: anyhow::Result<Res>,\n}\n\nimpl<Res: Default> Default for CacheEntry<Res> {\n  fn default() -> Self {\n    CacheEntry {\n      last_ts: 0,\n      res: Ok(Res::default()),\n    }\n  }\n}\n\nimpl<Res: Clone> CacheEntry<Res> {\n  pub fn set(&mut self, res: &anyhow::Result<Res>, timestamp: i64) {\n    self.res = res.as_ref().map_err(clone_anyhow_error).cloned();\n    self.last_ts = timestamp;\n  }\n\n  pub fn clone_res(&self) -> anyhow::Result<Res> {\n    self.res.as_ref().map_err(clone_anyhow_error).cloned()\n  }\n}\n\nfn clone_anyhow_error(e: &anyhow::Error) -> anyhow::Error {\n  let mut reasons =\n    e.chain().map(|e| e.to_string()).collect::<Vec<_>>();\n  // Always guaranteed to be at least one reason\n  // Need to start the chain with the last reason\n  let mut e = anyhow::Error::msg(reasons.pop().unwrap());\n  // Need to reverse reason application from lowest context to highest context.\n  for reason in reasons.into_iter().rev() {\n    e = e.context(reason)\n  }\n  e\n}\n"
  },
  {
    "path": "lib/command/Cargo.toml",
    "content": "[package]\nname = \"command\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[dependencies]\nkomodo_client.workspace = true\nrun_command.workspace = true\nsvi.workspace = true"
  },
  {
    "path": "lib/command/README.md",
    "content": "# Command module\n\nHelpers to run shell commands as child processes, and collect the outputs."
  },
  {
    "path": "lib/command/src/lib.rs",
    "content": "use std::path::Path;\n\nuse komodo_client::{\n  entities::{komodo_timestamp, update::Log},\n  parsers::parse_multiline_command,\n};\nuse run_command::{CommandOutput, async_run_command};\n\npub async fn run_komodo_command(\n  stage: &str,\n  path: impl Into<Option<&Path>>,\n  command: impl AsRef<str>,\n) -> Log {\n  let command = if let Some(path) = path.into() {\n    format!(\"cd {} && {}\", path.display(), command.as_ref())\n  } else {\n    command.as_ref().to_string()\n  };\n  let start_ts = komodo_timestamp();\n  let output = async_run_command(&command).await;\n  output_into_log(stage, command, start_ts, output)\n}\n\n/// Parses commands out of multiline string\n/// and chains them together with '&&'.\n/// Supports full line and end of line comments.\n/// See [parse_multiline_command].\n///\n/// The result may be None if the command is empty after parsing,\n/// ie if all the lines are commented out.\npub async fn run_komodo_command_multiline(\n  stage: &str,\n  path: impl Into<Option<&Path>>,\n  command: impl AsRef<str>,\n) -> Option<Log> {\n  let command = parse_multiline_command(command);\n  if command.is_empty() {\n    return None;\n  }\n  Some(run_komodo_command(stage, path, command).await)\n}\n\n/// Executes the command, and sanitizes the output to avoid exposing secrets in the log.\n///\n/// Checks to make sure the command is non-empty after being multiline-parsed.\n///\n/// If `parse_multiline: true`, parses commands out of multiline string\n/// and chains them together with '&&'.\n/// Supports full line and end of line comments.\n/// See [parse_multiline_command].\npub async fn run_komodo_command_with_sanitization(\n  stage: &str,\n  path: impl Into<Option<&Path>>,\n  command: impl AsRef<str>,\n  parse_multiline: bool,\n  replacers: &[(String, String)],\n) -> Option<Log> {\n  let mut log = if parse_multiline {\n    run_komodo_command_multiline(stage, path, command).await\n  } else {\n    run_komodo_command(stage, path, command).await.into()\n  }?;\n\n  // Sanitize the command and output\n  log.command = svi::replace_in_string(&log.command, replacers);\n  log.stdout = svi::replace_in_string(&log.stdout, replacers);\n  log.stderr = svi::replace_in_string(&log.stderr, replacers);\n\n  Some(log)\n}\n\npub fn output_into_log(\n  stage: &str,\n  command: String,\n  start_ts: i64,\n  output: CommandOutput,\n) -> Log {\n  let success = output.success();\n  Log {\n    stage: stage.to_string(),\n    stdout: output.stdout,\n    stderr: output.stderr,\n    command,\n    success,\n    start_ts,\n    end_ts: komodo_timestamp(),\n  }\n}\n"
  },
  {
    "path": "lib/config/Cargo.toml",
    "content": "[package]\nname = \"config\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[dependencies]\nserde_json.workspace = true\nserde_yaml_ng.workspace = true\nthiserror.workspace = true\nindexmap.workspace = true\nwildcard.workspace = true\ncolored.workspace = true\nregex.workspace = true\nserde.workspace = true\ntoml.workspace = true"
  },
  {
    "path": "lib/config/src/error.rs",
    "content": "use std::path::PathBuf;\n\n#[derive(Debug, thiserror::Error)]\npub enum Error {\n  #[error(\n    \"Types on field {key} do not match | got {value:?}, expected object\"\n  )]\n  ObjectFieldTypeMismatch {\n    key: String,\n    value: serde_json::Value,\n  },\n\n  #[error(\n    \"Types on field {key} do not match | got {value:?}, expected array\"\n  )]\n  ArrayFieldTypeMismatch {\n    key: String,\n    value: serde_json::Value,\n  },\n\n  #[error(\"Failed to open file at {path} | {e:?}\")]\n  FileOpen { e: std::io::Error, path: PathBuf },\n\n  #[error(\"Failed to read contents of file at {path} | {e:?}\")]\n  ReadFileContents { e: std::io::Error, path: PathBuf },\n\n  #[error(\"Failed to parse toml file at {path} | {e:?}\")]\n  ParseToml { e: toml::de::Error, path: PathBuf },\n\n  #[error(\"Failed to parse yaml file at {path} | {e:?}\")]\n  ParseYaml {\n    e: serde_yaml_ng::Error,\n    path: PathBuf,\n  },\n\n  #[error(\"Failed to parse json file at {path} | {e:?}\")]\n  ParseJson { e: serde_json::Error, path: PathBuf },\n\n  #[error(\"Unsupported file type at {path}\")]\n  UnsupportedFileType { path: PathBuf },\n\n  #[error(\"Failed to parse merged config into final type | {e:?}\")]\n  ParseFinalJson { e: serde_json::Error },\n\n  #[error(\"Failed to serialize config to json string | {e:?}\")]\n  SerializeJson { e: serde_json::Error },\n\n  #[error(\"Failed to read directory at {path:?}\")]\n  ReadDir { path: PathBuf, e: std::io::Error },\n\n  #[error(\"Failed to get file handle for file in directory {path:?}\")]\n  DirFile { e: std::io::Error, path: PathBuf },\n\n  #[error(\"Failed to get file name for file at {path:?}\")]\n  GetFileName { path: PathBuf },\n\n  #[error(\"Failed to get metadata for path {path:?} | {e:?}\")]\n  ReadPathMetaData { path: PathBuf, e: std::io::Error },\n\n  #[error(\"Parsed value is not object\")]\n  ValueIsNotObject,\n}\n"
  },
  {
    "path": "lib/config/src/includes.rs",
    "content": "use std::{\n  collections::HashSet,\n  path::{Path, PathBuf},\n};\n\npub struct IncludesLoader {\n  includes: HashSet<PathBuf>,\n  include_file_name: &'static str,\n}\n\nimpl IncludesLoader {\n  pub fn new(include_file_name: &'static str) -> Self {\n    Self {\n      includes: HashSet::new(),\n      include_file_name,\n    }\n  }\n\n  pub fn init(path: &Path, include_file_name: &'static str) -> Self {\n    let mut includes = Self::new(include_file_name);\n    includes.load_more(path);\n    includes\n  }\n\n  pub fn finish(self) -> HashSet<PathBuf> {\n    self.includes\n  }\n\n  pub fn load_more(&mut self, folder: &Path) {\n    if !folder.is_dir() {\n      return;\n    }\n    let Ok(folder) = folder.canonicalize() else {\n      return;\n    };\n    // Add any includes in this folder\n    if let Ok(ignore) =\n      std::fs::read_to_string(folder.join(self.include_file_name))\n    {\n      self.includes.extend(\n        ignore\n          .split('\\n')\n          .map(|line| line.trim())\n          // Ignore empty / commented out lines\n          .filter(|line| !line.is_empty() && !line.starts_with('#'))\n          // Remove end of line comments\n          .map(|line| {\n            line\n              .split_once('#')\n              .map(|res| res.0.trim())\n              .unwrap_or(line)\n          })\n          .flat_map(|line| folder.join(line).canonicalize()),\n      );\n    };\n  }\n}\n"
  },
  {
    "path": "lib/config/src/lib.rs",
    "content": "//! # Komodo Config\n//!\n//! This library is used to parse Core, Periphery, and CLI config files.\n//! It supports interpolating in environment variables (only '${VAR}' syntax),\n//! as well as merging together multiple files into a final configuration object.\n\nuse std::path::Path;\n\nuse colored::Colorize;\nuse indexmap::IndexSet;\nuse serde::de::DeserializeOwned;\n\nmod error;\nmod includes;\nmod load;\nmod merge;\n\npub use error::Error;\npub use merge::{merge_config, merge_objects};\n\npub type Result<T> = ::core::result::Result<T, Error>;\n\n/// Set the configuration for loading config files.\npub struct ConfigLoader<'outer, 'inner> {\n  /// Paths to either files or directories\n  /// to include in the final configuration.\n  ///\n  /// Path coming later in the array (higher index) will override\n  /// configuration in earlier paths.\n  pub paths: &'outer [&'inner Path],\n  /// Wilcard patterns to match file names in given directories.\n  ///\n  /// Patterns coming later in the array (higher index) will override\n  /// configuration added by earlier patterns, however this is\n  /// only relavant for an individual `path`. Later `paths`\n  /// will still have higher priority.\n  pub match_wildcards: &'outer [&'inner str],\n  /// The file name to search for `.include` file.\n  pub include_file_name: &'static str,\n  /// Whether to merge nested config objects.\n  /// Otherwise, the object will be replaced at\n  /// the top-level key by the highest priority config file\n  /// in which it is specified.\n  pub merge_nested: bool,\n  /// Whether to extend array in configuration files.\n  /// Otherwise, the array will be replaced at\n  /// the top-level key by the highest priority config file\n  /// in which it is specified.\n  pub extend_array: bool,\n  /// Print some extra information on configuation load.\n  ///\n  /// Note. This is different than application level log level.\n  pub debug_print: bool,\n}\n\nimpl ConfigLoader<'_, '_> {\n  pub fn load<T: DeserializeOwned>(self) -> Result<T> {\n    let ConfigLoader {\n      paths,\n      match_wildcards,\n      include_file_name,\n      merge_nested,\n      extend_array,\n      debug_print,\n    } = self;\n    let mut wildcards = Vec::with_capacity(match_wildcards.len());\n    for &wc in match_wildcards {\n      match wildcard::Wildcard::new(wc.as_bytes()) {\n        Ok(wc) => wildcards.push(wc),\n        Err(e) => {\n          eprintln!(\n            \"{}: Keyword '{}' is invalid wildcard | {e:?}\",\n            \"ERROR\".red(),\n            wc.bold(),\n          );\n        }\n      }\n    }\n    let mut all_files = IndexSet::new();\n    for &path in paths {\n      let Ok(metadata) = std::fs::metadata(path) else {\n        continue;\n      };\n      if metadata.is_dir() {\n        let mut files = Vec::new();\n        load::load_config_files(\n          &mut files,\n          path,\n          &wildcards,\n          include_file_name,\n          debug_print,\n        );\n        files.sort_by(|(a_index, a_path), (b_index, b_path)| {\n          a_index.cmp(b_index).then(a_path.cmp(b_path))\n        });\n        all_files.extend(files.into_iter().map(|(_, path)| path));\n      } else if metadata.is_file() {\n        let path = path.to_path_buf();\n        // If the same path comes up again later on, it should be removed and\n        // reinserted so it maintains higher priority.\n        all_files.shift_remove(&path);\n        all_files.insert(path);\n      }\n    }\n    if debug_print {\n      println!(\n        \"{}: {}: {all_files:?}\",\n        \"DEBUG\".cyan(),\n        \"Found Files\".dimmed()\n      );\n    }\n    load::load_parse_config_files(\n      &all_files.into_iter().collect::<Vec<_>>(),\n      merge_nested,\n      extend_array,\n    )\n  }\n}\n"
  },
  {
    "path": "lib/config/src/load.rs",
    "content": "use std::{\n  fs::File,\n  io::Read,\n  path::{Path, PathBuf},\n};\n\nuse colored::Colorize;\nuse serde::de::DeserializeOwned;\n\nuse crate::{\n  Error, Result, includes::IncludesLoader, merge::merge_objects,\n};\n\npub fn load_config_files(\n  // stores index of matching keyword as well as path\n  files: &mut Vec<(usize, PathBuf)>,\n  path: &Path,\n  keywords: &[wildcard::Wildcard],\n  include_file_name: &'static str,\n  debug_print: bool,\n) {\n  // File base case.\n  if path.is_file() {\n    files.push((0, path.to_path_buf()));\n    return;\n  }\n\n  if !path.is_dir() {\n    return;\n  }\n\n  let Ok(folder) = path.canonicalize() else {\n    return;\n  };\n  let Ok(read_dir) = std::fs::read_dir(&folder) else {\n    return;\n  };\n\n  // Collect any config files in the current dir.\n  for dir_entry in read_dir.flatten() {\n    let path = dir_entry.path();\n    let Ok(metadata) = dir_entry.metadata() else {\n      continue;\n    };\n    if metadata.is_file() {\n      let file_name = dir_entry.file_name();\n      let Some(file_name) = file_name.to_str() else {\n        continue;\n      };\n      // Ensure file name matches a wildcard keyword\n      let index = if keywords.is_empty() {\n        0\n      } else if let Some(index) = keywords\n        .iter()\n        .position(|wc| wc.is_match(file_name.as_bytes()))\n      {\n        // actual config keyword matches will have higher priority than\n        // when files are added via the base case.\n        index + 1\n      } else {\n        continue;\n      };\n      let Ok(path) = path.canonicalize() else {\n        continue;\n      };\n      files.push((index, path));\n    }\n  }\n\n  // Collect any paths specified in 'includes'\n  let includes =\n    IncludesLoader::init(&folder, include_file_name).finish();\n  if includes.is_empty() {\n    return;\n  }\n\n  if debug_print {\n    println!(\n      \"{}: {}: {includes:?}\",\n      \"DEBUG\".cyan(),\n      format_args!(\n        \"{} {path:?} {}\",\n        \"Config Path\".dimmed(),\n        \"Includes\".dimmed()\n      ),\n    );\n  }\n\n  // Add these paths as well recursively.\n  for path in includes {\n    load_config_files(\n      files,\n      &path,\n      keywords,\n      include_file_name,\n      debug_print,\n    );\n  }\n}\n\n/// loads multiple config files\npub fn load_parse_config_files<T: DeserializeOwned>(\n  files: &[PathBuf],\n  merge_nested: bool,\n  extend_array: bool,\n) -> Result<T> {\n  let mut target = serde_json::Map::new();\n\n  for file in files {\n    let source = match load_parse_config_file(file) {\n      Ok(source) => source,\n      Err(e) => {\n        eprintln!(\"{}: {e}\", \"WARN\".yellow());\n        continue;\n      }\n    };\n    target = match merge_objects(\n      target.clone(),\n      source,\n      merge_nested,\n      extend_array,\n    ) {\n      Ok(target) => target,\n      Err(e) => {\n        eprint!(\"{}: {e}\", \"WARN\".yellow());\n        target\n      }\n    };\n  }\n\n  serde_json::from_value(serde_json::Value::Object(target))\n    .map_err(|e| Error::ParseFinalJson { e })\n}\n\n/// Loads and parses a single config file\npub fn load_parse_config_file<T: DeserializeOwned>(\n  file: &Path,\n) -> Result<T> {\n  let mut file_handle =\n    File::open(file).map_err(|e| Error::FileOpen {\n      e,\n      path: file.to_path_buf(),\n    })?;\n  let mut contents = String::new();\n  file_handle.read_to_string(&mut contents).map_err(|e| {\n    Error::ReadFileContents {\n      e,\n      path: file.to_path_buf(),\n    }\n  })?;\n  // Interpolate environment variables matching `${VAR}` syntax (not `$VAR` to avoid edge cases).\n  let contents = interpolate_env(&contents);\n  let config = match file.extension().and_then(|e| e.to_str()) {\n    Some(\"toml\") => {\n      toml::from_str(&contents).map_err(|e| Error::ParseToml {\n        e,\n        path: file.to_path_buf(),\n      })?\n    }\n    Some(\"yaml\") | Some(\"yml\") => serde_yaml_ng::from_str(&contents)\n      .map_err(|e| Error::ParseYaml {\n        e,\n        path: file.to_path_buf(),\n      })?,\n    Some(\"json\") => {\n      serde_json::from_reader(file_handle).map_err(|e| {\n        Error::ParseJson {\n          e,\n          path: file.to_path_buf(),\n        }\n      })?\n    }\n    Some(_) | None => {\n      return Err(Error::UnsupportedFileType {\n        path: file.to_path_buf(),\n      });\n    }\n  };\n  Ok(config)\n}\n\n/// Only supports '${VAR}' syntax\nfn interpolate_env(input: &str) -> String {\n  let re = regex::Regex::new(r\"\\$\\{([A-Za-z0-9_]+)\\}\").unwrap();\n  let first_pass = re\n    .replace_all(input, |caps: &regex::Captures| {\n      let var_name = &caps[1];\n      std::env::var(var_name).unwrap_or_default()\n    })\n    .into_owned();\n  // Do it twice in case any env vars expand again to env vars\n  re.replace_all(&first_pass, |caps: &regex::Captures| {\n    let var_name = &caps[1];\n    std::env::var(var_name).unwrap_or_default()\n  })\n  .into_owned()\n}\n"
  },
  {
    "path": "lib/config/src/merge.rs",
    "content": "use serde::{Serialize, de::DeserializeOwned};\n\nuse crate::{Error, Result};\n\n/// - Object is serde_json::Map<String, serde_json::Value>.\n/// - Source will overide target.\n/// - Will recurse when field is object if merge_object = true, otherwise object will be replaced.\n/// - Will extend when field is array if extend_array = true, otherwise array will be replaced.\n/// - Will return error when types on source and target fields do not match.\npub fn merge_objects(\n  mut target: serde_json::Map<String, serde_json::Value>,\n  source: serde_json::Map<String, serde_json::Value>,\n  merge_nested: bool,\n  extend_array: bool,\n) -> Result<serde_json::Map<String, serde_json::Value>> {\n  for (key, value) in source {\n    let Some(curr) = target.remove(&key) else {\n      target.insert(key, value);\n      continue;\n    };\n    match curr {\n      serde_json::Value::Object(target_obj) => {\n        if !merge_nested {\n          target.insert(key, value);\n          continue;\n        }\n        match value {\n          serde_json::Value::Object(source_obj) => {\n            target.insert(\n              key,\n              serde_json::Value::Object(merge_objects(\n                target_obj,\n                source_obj,\n                merge_nested,\n                extend_array,\n              )?),\n            );\n          }\n          _ => {\n            return Err(Error::ObjectFieldTypeMismatch {\n              key,\n              value,\n            });\n          }\n        }\n      }\n      serde_json::Value::Array(mut target_arr) => {\n        if !extend_array {\n          target.insert(key, value);\n          continue;\n        }\n        match value {\n          serde_json::Value::Array(source_arr) => {\n            target_arr.extend(source_arr);\n            target.insert(key, serde_json::Value::Array(target_arr));\n          }\n          _ => {\n            return Err(Error::ArrayFieldTypeMismatch { key, value });\n          }\n        }\n      }\n      _ => {\n        target.insert(key, value);\n      }\n    }\n  }\n  Ok(target)\n}\n\n/// Source will overide target\npub fn merge_config<T: Serialize + DeserializeOwned>(\n  target: T,\n  source: T,\n  merge_nested: bool,\n  extend_array: bool,\n) -> Result<T> {\n  let serde_json::Value::Object(target) =\n    serde_json::to_value(target)\n      .map_err(|e| Error::SerializeJson { e })?\n  else {\n    return Err(Error::ValueIsNotObject);\n  };\n  let serde_json::Value::Object(source) =\n    serde_json::to_value(source)\n      .map_err(|e| Error::SerializeJson { e })?\n  else {\n    return Err(Error::ValueIsNotObject);\n  };\n  let object =\n    merge_objects(target, source, merge_nested, extend_array)?;\n  serde_json::from_value(serde_json::Value::Object(object))\n    .map_err(|e| Error::ParseFinalJson { e })\n}\n"
  },
  {
    "path": "lib/database/Cargo.toml",
    "content": "[package]\nname = \"database\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[dependencies]\n# local\nkomodo_client = { workspace = true, features = [\"mongo\"] }\n# mogh\nmongo_indexed.workspace = true\nmungos.workspace = true\n# external\nasync-compression.workspace = true\nfutures-util.workspace = true\nserde_json.workspace = true\ntokio-util.workspace = true\ntracing.workspace = true\nanyhow.workspace = true\nbcrypt.workspace = true\nchrono.workspace = true\ntokio.workspace = true"
  },
  {
    "path": "lib/database/src/lib.rs",
    "content": "use std::str::FromStr;\n\nuse anyhow::{Context, anyhow};\nuse komodo_client::entities::{\n  action::Action,\n  alert::Alert,\n  alerter::Alerter,\n  api_key::ApiKey,\n  build::Build,\n  builder::Builder,\n  config::DatabaseConfig,\n  deployment::Deployment,\n  permission::Permission,\n  procedure::Procedure,\n  provider::{DockerRegistryAccount, GitProviderAccount},\n  repo::Repo,\n  server::Server,\n  stack::Stack,\n  stats::SystemStatsRecord,\n  sync::ResourceSync,\n  tag::Tag,\n  update::Update,\n  user::{User, UserConfig},\n  user_group::UserGroup,\n  variable::Variable,\n};\nuse mongo_indexed::{create_index, create_unique_index};\nuse mungos::{\n  init::MongoBuilder,\n  mongodb::{\n    Collection, Database,\n    bson::{doc, oid::ObjectId},\n  },\n};\n\npub use mongo_indexed;\npub use mungos;\n\npub mod utils;\n\n#[derive(Debug)]\npub struct Client {\n  pub users: Collection<User>,\n  pub user_groups: Collection<UserGroup>,\n  pub permissions: Collection<Permission>,\n  pub api_keys: Collection<ApiKey>,\n  pub tags: Collection<Tag>,\n  pub variables: Collection<Variable>,\n  pub git_accounts: Collection<GitProviderAccount>,\n  pub registry_accounts: Collection<DockerRegistryAccount>,\n  pub updates: Collection<Update>,\n  pub alerts: Collection<Alert>,\n  pub stats: Collection<SystemStatsRecord>,\n  // RESOURCES\n  pub servers: Collection<Server>,\n  pub deployments: Collection<Deployment>,\n  pub builds: Collection<Build>,\n  pub builders: Collection<Builder>,\n  pub repos: Collection<Repo>,\n  pub procedures: Collection<Procedure>,\n  pub actions: Collection<Action>,\n  pub alerters: Collection<Alerter>,\n  pub resource_syncs: Collection<ResourceSync>,\n  pub stacks: Collection<Stack>,\n  //\n  pub db: Database,\n}\n\nimpl Client {\n  pub async fn new(\n    config: &DatabaseConfig,\n  ) -> anyhow::Result<Client> {\n    let db = init(config).await?;\n    Self::from_database(db).await\n  }\n\n  pub async fn from_database(db: Database) -> anyhow::Result<Client> {\n    let client = Client {\n      users: mongo_indexed::collection(&db, true).await?,\n      user_groups: mongo_indexed::collection(&db, true).await?,\n      permissions: mongo_indexed::collection(&db, true).await?,\n      api_keys: mongo_indexed::collection(&db, true).await?,\n      tags: mongo_indexed::collection(&db, true).await?,\n      variables: mongo_indexed::collection(&db, true).await?,\n      git_accounts: mongo_indexed::collection(&db, true).await?,\n      registry_accounts: mongo_indexed::collection(&db, true).await?,\n      updates: mongo_indexed::collection(&db, true).await?,\n      alerts: mongo_indexed::collection(&db, true).await?,\n      stats: mongo_indexed::collection(&db, true).await?,\n      // RESOURCES\n      servers: resource_collection(&db, \"Server\").await?,\n      deployments: resource_collection(&db, \"Deployment\").await?,\n      builds: resource_collection(&db, \"Build\").await?,\n      builders: resource_collection(&db, \"Builder\").await?,\n      repos: resource_collection(&db, \"Repo\").await?,\n      alerters: resource_collection(&db, \"Alerter\").await?,\n      procedures: resource_collection(&db, \"Procedure\").await?,\n      actions: resource_collection(&db, \"Action\").await?,\n      resource_syncs: resource_collection(&db, \"ResourceSync\")\n        .await?,\n      stacks: resource_collection(&db, \"Stack\").await?,\n      //\n      db,\n    };\n    Ok(client)\n  }\n\n  /// Updates a user's password using a DB call.\n  pub async fn set_user_password(\n    &self,\n    user: &User,\n    password: &str,\n  ) -> anyhow::Result<()> {\n    let UserConfig::Local { .. } = user.config else {\n      return Err(anyhow!(\n        \"User is not a 'Local' (username / password) user\"\n      ));\n    };\n    if password.is_empty() {\n      return Err(anyhow!(\"Password cannot be empty.\"));\n    }\n    let id = ObjectId::from_str(&user.id)\n      .context(\"User id not valid ObjectId.\")?;\n    let hashed_password =\n      hash_password(password).context(\"Failed to hash password\")?;\n    self\n      .users\n      .update_one(\n        doc! { \"_id\": id },\n        doc! { \"$set\": {\n          \"config.data.password\": hashed_password\n        } },\n      )\n      .await\n      .context(\"Failed to update user password on database.\")?;\n    Ok(())\n  }\n}\n\n/// Initializes unindexed database handle.\npub async fn init(\n  DatabaseConfig {\n    uri,\n    address,\n    username,\n    password,\n    app_name,\n    db_name,\n  }: &DatabaseConfig,\n) -> anyhow::Result<Database> {\n  let mut client = MongoBuilder::default().app_name(app_name);\n\n  match (\n    !uri.is_empty(),\n    !address.is_empty(),\n    !username.is_empty(),\n    !password.is_empty(),\n  ) {\n    (true, _, _, _) => {\n      client = client.uri(uri);\n    }\n    (_, true, true, true) => {\n      client = client\n        .address(address)\n        .username(username)\n        .password(password);\n    }\n    (_, true, _, _) => {\n      client = client.address(address);\n    }\n    _ => {\n      return Err(anyhow!(\n        \"'config.database' not configured correctly. must pass either 'config.database.uri', or 'config.database.address' + 'config.database.username' + 'config.database.password'\"\n      ));\n    }\n  }\n\n  let client = client\n    .build()\n    .await\n    .context(\"Failed to initialize database connection.\")?;\n\n  Ok(client.database(db_name))\n}\n\nasync fn resource_collection<T: Send + Sync>(\n  db: &Database,\n  collection_name: &str,\n) -> anyhow::Result<Collection<T>> {\n  let coll = db.collection::<T>(collection_name);\n\n  create_unique_index(&coll, \"name\").await?;\n\n  create_index(&coll, \"tags\").await?;\n\n  Ok(coll)\n}\n\nconst BCRYPT_COST: u32 = 10;\npub fn hash_password<P>(password: P) -> anyhow::Result<String>\nwhere\n  P: AsRef<[u8]>,\n{\n  bcrypt::hash(password, BCRYPT_COST)\n    .context(\"failed to hash password\")\n}\n"
  },
  {
    "path": "lib/database/src/utils/backup.rs",
    "content": "use std::{\n  path::Path,\n  sync::{Arc, atomic},\n};\n\nuse anyhow::{Context, anyhow};\nuse async_compression::tokio::write::GzipEncoder;\nuse chrono::Local;\nuse futures_util::{\n  SinkExt, StreamExt, TryStreamExt, stream::FuturesUnordered,\n};\nuse mungos::mongodb::{\n  Database,\n  bson::{Document, RawDocumentBuf},\n};\nuse tokio::io::{AsyncWriteExt, BufWriter};\nuse tokio_util::codec::{FramedWrite, LinesCodec};\nuse tracing::{error, info, warn};\n\npub async fn backup(\n  db: &Database,\n  backups_folder: &Path,\n) -> anyhow::Result<()> {\n  let collections = db\n    .list_collection_names()\n    .await\n    .context(\"Failed to list collections on source db\")?;\n\n  let now_backups_folder = backups_folder\n    .join(Local::now().format(\"%Y-%m-%d_%H-%M-%S\").to_string());\n\n  tokio::fs::create_dir_all(&now_backups_folder)\n    .await\n    .context(\"Failed to create backup folder\")?;\n\n  info!(\"Backing up to {now_backups_folder:?}...\");\n\n  let has_error = Arc::new(atomic::AtomicBool::new(false));\n\n  let mut handles = collections\n    .into_iter()\n    .map(|collection| {\n      let source = db.collection::<RawDocumentBuf>(&collection);\n      let file_path = if collection == \"Stats\" {\n        backups_folder.join(\"Stats.gz\")\n      } else {\n        now_backups_folder.join(format!(\"{collection}.gz\"))\n      };\n      let has_error = has_error.clone();\n      tokio::spawn(async move {\n        let res = async {\n          let mut count = 0;\n          let _ = tokio::fs::remove_file(&file_path).await;\n          let file =\n            tokio::fs::File::create(&file_path).await.with_context(\n              || format!(\"Failed to create file at {file_path:?}\"),\n            )?;\n          let mut writer = FramedWrite::new(\n            BufWriter::new(GzipEncoder::with_quality(\n              file,\n              async_compression::Level::Best,\n            )),\n            LinesCodec::new(),\n          );\n          let mut cursor = source\n            .find(Document::new())\n            .await\n            .context(\"Failed to query source collection\")?;\n          while let Some(doc) = cursor\n            .try_next()\n            .await\n            .context(\"Failed to get next document\")?\n          {\n            count += 1;\n            let str = match serde_json::to_string(&doc)\n              .context(\"Failed to serialize document\")\n            {\n              Ok(str) => str,\n              Err(e) => {\n                warn!(\"{e:#}\");\n                continue;\n              }\n            };\n            if let Err(e) = writer\n              .send(str)\n              .await\n              .context(\"Failed to write document to file\")\n            {\n              warn!(\"{e:#}\");\n            }\n          }\n\n          if let Err(e) = <_ as SinkExt<String>>::flush(&mut writer)\n            .await\n            .context(\"Failed to flush writer\")\n          {\n            error!(\"{e:#}\");\n          };\n\n          if let Err(e) = writer\n            .into_inner()\n            .shutdown()\n            .await\n            .context(\"Failed to shutdown writer compression\")\n          {\n            error!(\"{e:#}\");\n          }\n\n          anyhow::Ok(count)\n        }\n        .await;\n        match res {\n          Ok(count) => {\n            if count > 0 {\n              info!(\"[{collection}]: Backed up {count} items\");\n            }\n          }\n          Err(e) => {\n            error!(\"[{collection}]: {e:#}\");\n            has_error.store(true, atomic::Ordering::Relaxed);\n          }\n        }\n      })\n    })\n    .collect::<FuturesUnordered<_>>();\n\n  loop {\n    match handles.next().await {\n      Some(Ok(())) => {}\n      Some(Err(e)) => {\n        error!(\"{e:#}\");\n      }\n      None => break,\n    }\n  }\n\n  if has_error.load(atomic::Ordering::Relaxed) {\n    Err(anyhow!(\"Finished backing up database with errors 🚨\"))\n  } else {\n    info!(\"Finished backing up database ✅\");\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "lib/database/src/utils/copy.rs",
    "content": "use anyhow::Context;\nuse futures_util::{\n  StreamExt, TryStreamExt, stream::FuturesUnordered,\n};\nuse mungos::{\n  bulk_update::{BulkUpdate, bulk_update_retry_too_big},\n  mongodb::{\n    Database,\n    bson::{Document, doc},\n  },\n};\nuse tracing::{error, info};\n\npub async fn copy(\n  source_db: &Database,\n  target_db: &Database,\n) -> anyhow::Result<()> {\n  let mut handles = source_db\n    .list_collection_names()\n    .await\n    .context(\"Failed to list collections on source db\")?.into_iter().map(|collection| {\n      let source = source_db.collection::<Document>(&collection);\n      let target_db = target_db.clone();\n      tokio::spawn(async move {\n        let res = async {\n          let mut buffer = Vec::<BulkUpdate>::new();\n          // The update collection is bigger than others,\n          // can hit the max bson limit on the bulk upsert call without this.\n          let max_buffer = if collection == \"Update\" {\n            1_000\n          } else {\n            10_000\n          };\n          let mut count = 0;\n          let mut cursor = source\n            .find(Document::new())\n            .await\n            .context(\"Failed to query source collection\")?;\n          while let Some(document) = cursor\n            .try_next()\n            .await\n            .context(\"Failed to get next document\")?\n          {\n            let Some(id) = document.get(\"_id\").and_then(|id| id.as_object_id()) else {\n              continue;\n            };\n            count += 1;\n            buffer.push(BulkUpdate { query: doc! { \"_id\": id }, update: doc! { \"$set\": document } });\n            if buffer.len() >= max_buffer {\n              if let Err(e) = bulk_update_retry_too_big(&target_db, &collection, &buffer, true).await.context(\"Failed to flush documents\")\n              {\n                error!(\"Failed to flush document batch in {collection} collection | {e:#}\");\n              };\n              buffer.clear();\n            }\n          }\n          if !buffer.is_empty() {\n            bulk_update_retry_too_big(&target_db, &collection, &buffer, true)\n              .await\n              .context(\"Failed to flush documents\")?;\n          }\n          anyhow::Ok(count)\n        }\n        .await;\n        match res {\n          Ok(count) => {\n            if count > 0 {\n              info!(\"Finished copying {collection} collection | Copied {count}\");\n            }\n          }\n          Err(e) => {\n            error!(\"Failed to copy {collection} collection | {e:#}\")\n          }\n        }\n      })\n    }).collect::<FuturesUnordered<_>>();\n\n  loop {\n    match handles.next().await {\n      Some(Ok(())) => {}\n      Some(Err(e)) => {\n        error!(\"{e:#}\");\n      }\n      None => break,\n    }\n  }\n\n  info!(\"Finished copying database ✅\");\n\n  Ok(())\n}\n"
  },
  {
    "path": "lib/database/src/utils/mod.rs",
    "content": "mod backup;\nmod copy;\nmod restore;\n\npub use backup::backup;\npub use copy::copy;\npub use restore::restore;\n"
  },
  {
    "path": "lib/database/src/utils/restore.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse anyhow::Context;\nuse async_compression::tokio::bufread::GzipDecoder;\nuse futures_util::{\n  StreamExt, TryStreamExt, stream::FuturesUnordered,\n};\nuse mungos::{\n  bulk_update::{BulkUpdate, bulk_update_retry_too_big},\n  mongodb::{\n    Database,\n    bson::{Document, doc},\n  },\n};\nuse tokio::io::BufReader;\nuse tokio_util::codec::{FramedRead, LinesCodec};\nuse tracing::{error, info, warn};\n\npub async fn restore(\n  db: &Database,\n  backups_folder: &Path,\n  restore_folder: Option<&Path>,\n) -> anyhow::Result<()> {\n  // Get the specific dated folder to restore contents of\n  let restore_folder = if let Some(restore_folder) = restore_folder {\n    backups_folder.join(restore_folder)\n  } else {\n    latest_restore_folder(backups_folder).await?\n  }\n  .components()\n  .collect::<PathBuf>();\n\n  info!(\"Restore folder: {restore_folder:?}\");\n\n  let restore_files =\n    get_restore_files(backups_folder, &restore_folder).await?;\n\n  let mut handles = restore_files\n    .into_iter()\n    .map(|(collection, restore_file)| {\n      let db = db.clone();\n      async {\n        let col = collection.clone();\n        tokio::join!(\n          async { col },\n          tokio::spawn(async move {\n            let res = async {\n              let mut buffer = Vec::<BulkUpdate>::new();\n              // The update collection is bigger than others,\n              // can hit the max bson limit on the bulk upsert call without this.\n              let max_buffer = if collection == \"Update\" {\n                1_000\n              } else {\n                10_000\n              };\n              let mut count = 0;\n\n              let file = tokio::fs::File::open(&restore_file)\n                .await\n                .with_context(|| format!(\"Failed to open file {restore_file:?}\"))?;\n\n              let mut reader = FramedRead::new(\n                GzipDecoder::new(BufReader::new(file)),\n                LinesCodec::new()\n              );\n\n              while let Some(line) = reader.try_next()\n                .await\n                .context(\"Failed to get next line\")?\n              {\n                if line.is_empty() {\n                  continue;\n                }\n                let document = match serde_json::from_str::<Document>(&line)\n                  .context(\"Failed to deserialize line\")\n                {\n                  Ok(doc) => doc,\n                  Err(e) => {\n                    warn!(\"{e:#}\");\n                    continue;\n                  }\n                };\n                let Some(id) = document.get(\"_id\").and_then(|id| id.as_object_id()) else {\n                  continue;\n                };\n                count += 1;\n                buffer.push(BulkUpdate { query: doc! { \"_id\": id }, update: doc! { \"$set\": document } });\n                if buffer.len() >= max_buffer {\n                  if let Err(e) = bulk_update_retry_too_big(&db, &collection, &buffer, true).await.context(\"Failed to flush documents\")\n                  {\n                    error!(\"Failed to flush document batch in {collection} collection | {e:#}\");\n                  };\n                  buffer.clear();\n                }\n              }\n              if !buffer.is_empty() {\n                bulk_update_retry_too_big(&db, &collection, &buffer, true).await.context(\"Failed to flush documents\")?;\n              }\n              anyhow::Ok(count)\n            }.await;\n            match res {\n              Ok(count) => {\n                if count > 0 {\n                  info!(\"[{collection}]: Restored {count} items\");\n                }\n              }\n              Err(e) => {\n                error!(\"[{collection}]: {e:#}\");\n              }\n            }\n          })\n        )\n      }\n    })\n    .collect::<FuturesUnordered<_>>();\n\n  loop {\n    match handles.next().await {\n      Some((_collection, Ok(()))) => {}\n      Some((collection, Err(e))) => {\n        error!(\"[{collection}]: {e:#}\");\n      }\n      None => break,\n    }\n  }\n\n  info!(\"Finished restoring database ✅\");\n\n  Ok(())\n}\n\nasync fn latest_restore_folder(\n  backups_folder: &Path,\n) -> anyhow::Result<PathBuf> {\n  let mut max = PathBuf::new();\n  let mut backups_dir = tokio::fs::read_dir(backups_folder)\n    .await\n    .context(\"Failed to read backup directory\")?;\n  loop {\n    match backups_dir\n      .next_entry()\n      .await\n      .context(\"Failed to read backup dir entry\")\n    {\n      Ok(Some(entry)) => {\n        let path = entry.path();\n        if path.is_dir() && path > max {\n          max = path;\n        }\n      }\n      Ok(None) => break,\n      Err(e) => {\n        warn!(\"{e:#}\");\n        continue;\n      }\n    }\n  }\n  Ok(max.components().collect())\n}\n\nasync fn get_restore_files(\n  backups_folder: &Path,\n  restore_folder: &Path,\n) -> anyhow::Result<Vec<(String, PathBuf)>> {\n  let mut restore_dir =\n    tokio::fs::read_dir(restore_folder).await.with_context(|| {\n      format!(\"Failed to read restore directory {restore_folder:?}\")\n    })?;\n\n  let mut restore_files: Vec<(String, PathBuf)> = vec![(\n    String::from(\"Stats\"),\n    backups_folder.join(\"Stats.gz\").components().collect(),\n  )];\n\n  loop {\n    match restore_dir\n      .next_entry()\n      .await\n      .context(\"Failed to read restore dir entry\")\n    {\n      Ok(Some(file)) => {\n        let path = file.path();\n        let Some(file_name) = path.file_name() else {\n          continue;\n        };\n        let Some(file_name) = file_name.to_str() else {\n          continue;\n        };\n        let Some(collection) = file_name.strip_suffix(\".gz\") else {\n          continue;\n        };\n        restore_files.push((\n          collection.to_string(),\n          path.components().collect(),\n        ));\n      }\n      Ok(None) => break,\n      Err(e) => {\n        warn!(\"{e:#}\");\n        continue;\n      }\n    }\n  }\n\n  Ok(restore_files)\n}\n"
  },
  {
    "path": "lib/environment/Cargo.toml",
    "content": "[package]\nname = \"environment\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[dependencies]\nkomodo_client.workspace = true\nformatting.workspace = true\n#\nanyhow.workspace = true\ntokio.workspace = true"
  },
  {
    "path": "lib/environment/src/lib.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse anyhow::Context;\nuse formatting::format_serror;\nuse komodo_client::entities::{EnvironmentVar, update::Log};\n\n/// If the environment was written and needs to be passed to the compose command,\n/// will return the env file PathBuf.\n/// Should ensure all logs are successful after calling.\npub async fn write_env_file(\n  environment: &[EnvironmentVar],\n  folder: &Path,\n  env_file_path: &str,\n  logs: &mut Vec<Log>,\n) -> Option<PathBuf> {\n  let env_file_path =\n    folder.join(env_file_path).components().collect::<PathBuf>();\n\n  if environment.is_empty() {\n    // Still want to return Some(env_file_path) if the path\n    // already exists on the host and is a file.\n    // This is for \"Files on Server\" mode when user writes the env file themself.\n    if env_file_path.is_file() {\n      return Some(env_file_path);\n    }\n    return None;\n  }\n\n  let contents = environment\n    .iter()\n    .map(|env| format!(\"{}={}\", env.variable, env.value))\n    .collect::<Vec<_>>()\n    .join(\"\\n\");\n\n  if let Some(parent) = env_file_path.parent()\n    && let Err(e) = tokio::fs::create_dir_all(parent)\n      .await\n      .with_context(|| format!(\"Failed to initialize environment file parent directory {parent:?}\"))\n  {\n    logs.push(Log::error(\n      \"Write Environment File\",\n      format_serror(&e.into()),\n    ));\n    return None;\n  }\n\n  if let Err(e) = tokio::fs::write(&env_file_path, contents)\n    .await\n    .with_context(|| {\n      format!(\"Failed to write environment file to {env_file_path:?}\")\n    })\n  {\n    logs.push(Log::error(\n      \"Write Environment File\",\n      format_serror(&e.into()),\n    ));\n    return None;\n  }\n\n  logs.push(Log::simple(\n    \"Write Environment File\",\n    format!(\"Environment file written to {env_file_path:?}\"),\n  ));\n\n  Some(env_file_path)\n}\n"
  },
  {
    "path": "lib/environment_file/Cargo.toml",
    "content": "[package]\nname = \"environment_file\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[dependencies]\nthiserror.workspace = true"
  },
  {
    "path": "lib/environment_file/README.md",
    "content": "# Environment file module\n\nHelpers for parsing variables from file contents.\n\nUsed to parse secrets from the files specified in env variable ending in `_FILE`.\n\nCompatible with docker compose secrets,\nsee [https://docs.docker.com/compose/how-tos/use-secrets/](https://docs.docker.com/compose/how-tos/use-secrets/)."
  },
  {
    "path": "lib/environment_file/src/lib.rs",
    "content": "use std::{\n  path::{Path, PathBuf},\n  str::FromStr,\n};\n\n/// NOTE. This function will panic if file is non-None and fails to read file contents\npub fn maybe_read_item_from_file<T: FromStrDebugErr>(\n  var_file: Option<PathBuf>,\n  var: Option<T>,\n) -> Option<T> {\n  let Some(path) = var_file else { return var };\n  let res = std::fs::read_to_string(&path)\n    .map_err(|err| Error::<T>::ReadFileError {\n      path: path.clone(),\n      err,\n    })\n    .unwrap();\n  let res = T::from_str(res.trim())\n    .map_err(|err| Error::<T>::ParseValueError {\n      path,\n      err: err.into(),\n    })\n    .unwrap();\n  Some(res)\n}\n\n/// NOTE. This function will panic if file is non-None and fails to read file contents\npub fn maybe_read_list_from_file<T: FromStrDebugErr>(\n  var_file: Option<PathBuf>,\n  var: Option<Vec<T>>,\n) -> Option<Vec<T>> {\n  let Some(path) = var_file else { return var };\n  Some(parse_list_from_file(&path).unwrap())\n}\n\npub trait FromStrDebugErr: FromStr + std::fmt::Debug {\n  type Error: std::fmt::Debug + From<Self::Err>;\n}\n\nimpl FromStrDebugErr for String {\n  type Error = <String as FromStr>::Err;\n}\n\nimpl FromStrDebugErr for i64 {\n  type Error = <i64 as FromStr>::Err;\n}\n\n#[derive(Debug, thiserror::Error)]\nenum Error<T: std::fmt::Debug + FromStrDebugErr> {\n  #[error(\"Failed to read file contents from {path:?} | {err:?}\")]\n  ReadFileError { path: PathBuf, err: std::io::Error },\n  #[error(\"Failed to parse file contents from {path:?} | {err:?}\")]\n  ParseValueError { path: PathBuf, err: T::Error },\n}\n\nfn parse_list_from_file<T: FromStrDebugErr>(\n  path: &Path,\n) -> Result<Vec<T>, Error<T>> {\n  std::fs::read_to_string(path)\n    .map_err(|err| Error::ReadFileError {\n      path: path.to_path_buf(),\n      err,\n    })?\n    .split(',')\n    .map(str::trim)\n    .map(|s| {\n      T::from_str(s).map_err(|err| Error::ParseValueError {\n        path: path.to_path_buf(),\n        err: err.into(),\n      })\n    })\n    .collect::<Result<Vec<_>, Error<_>>>()\n}\n"
  },
  {
    "path": "lib/formatting/Cargo.toml",
    "content": "[package]\nname = \"formatting\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[dependencies]\nserror.workspace = true"
  },
  {
    "path": "lib/formatting/README.md",
    "content": "# Formatting module\n\nUsed to pretty-format logs using HTML for better display from the UI."
  },
  {
    "path": "lib/formatting/src/lib.rs",
    "content": "use serror::Serror;\n\npub fn muted(content: impl std::fmt::Display) -> String {\n  format!(\"<span class=\\\"text-muted-foreground\\\">{content}</span>\")\n}\n\npub fn bold(content: impl std::fmt::Display) -> String {\n  format!(\"<span class=\\\"font-bold\\\">{content}</span>\")\n}\n\npub fn colored(\n  content: impl std::fmt::Display,\n  color: Color,\n) -> String {\n  format!(\"<span class=\\\"{color}\\\">{content}</span>\")\n}\n\npub enum Color {\n  Red,\n  Green,\n  Blue,\n}\n\nimpl std::fmt::Display for Color {\n  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n    match self {\n      Color::Red => f.write_str(\"text-red-700 dark:text-red-400\"),\n      Color::Green => {\n        f.write_str(\"text-green-700 dark:text-green-400\")\n      }\n      Color::Blue => f.write_str(\"text-blue-700 dark:text-blue-400\"),\n    }\n  }\n}\n\npub fn format_serror(Serror { error, trace }: &Serror) -> String {\n  let trace = if !trace.is_empty() {\n    let mut out = format!(\"\\n\\n{}:\", muted(\"TRACE\"));\n\n    for (i, msg) in trace.iter().enumerate() {\n      out.push_str(&format!(\"\\n\\t{}: {msg}\", muted(i + 1)));\n    }\n\n    out\n  } else {\n    Default::default()\n  };\n  format!(\"{}: {error}{trace}\", colored(\"ERROR\", Color::Red))\n}\n"
  },
  {
    "path": "lib/git/Cargo.toml",
    "content": "[package]\nname = \"git\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[dependencies]\nkomodo_client.workspace = true\nformatting.workspace = true\ncommand.workspace = true\ncache.workspace = true\n#\nrun_command.workspace = true\n#\ntracing.workspace = true\nanyhow.workspace = true\ntokio.workspace = true"
  },
  {
    "path": "lib/git/README.md",
    "content": "# Git module\n\nHelpers for cloning, pulling, and committing to git repos."
  },
  {
    "path": "lib/git/src/clone.rs",
    "content": "use std::{io::ErrorKind, path::Path};\n\nuse anyhow::Context;\nuse command::run_komodo_command;\nuse formatting::format_serror;\nuse komodo_client::entities::{\n  RepoExecutionArgs, RepoExecutionResponse, all_logs_success,\n  update::Log,\n};\n\nuse crate::get_commit_hash_log;\n\n/// Will delete the existing repo folder,\n/// clone the repo, get the latest hash / message,\n/// and run on_clone / on_pull.\n///\n/// Assumes all interpolation is already done and takes the list of replacers\n/// for the On Clone command.\n#[tracing::instrument(\n  level = \"debug\",\n  skip(clone_args, access_token)\n)]\npub async fn clone<T>(\n  clone_args: T,\n  root_repo_dir: &Path,\n  access_token: Option<String>,\n) -> anyhow::Result<RepoExecutionResponse>\nwhere\n  T: Into<RepoExecutionArgs> + std::fmt::Debug,\n{\n  let args: RepoExecutionArgs = clone_args.into();\n  let repo_url = args.remote_url(access_token.as_deref())?;\n\n  let mut res = RepoExecutionResponse {\n    path: args.path(root_repo_dir),\n    logs: Vec::new(),\n    commit_hash: None,\n    commit_message: None,\n  };\n\n  // Ensure parent folder exists\n  if let Some(parent) = res.path.parent()\n    && let Err(e) = tokio::fs::create_dir_all(parent)\n      .await\n      .context(\"Failed to create clone parent directory.\")\n  {\n    res.logs.push(Log::error(\n      \"Prepare Repo Root\",\n      format_serror(&e.into()),\n    ));\n    return Ok(res);\n  }\n\n  match tokio::fs::remove_dir_all(&res.path).await {\n    Err(e) if e.kind() != ErrorKind::NotFound => {\n      let e: anyhow::Error = e.into();\n      res.logs.push(Log::error(\n        \"Clean Repo Root\",\n        format_serror(\n          &e.context(\n            \"Failed to remove existing repo root before clone.\",\n          )\n          .into(),\n        ),\n      ));\n      return Ok(res);\n    }\n    _ => {}\n  }\n\n  let command = format!(\n    \"git clone {repo_url} {} -b {}\",\n    res.path.display(),\n    args.branch\n  );\n\n  let mut log = run_komodo_command(\"Clone Repo\", None, command).await;\n\n  if let Some(token) = access_token {\n    log.command = log.command.replace(&token, \"<TOKEN>\");\n    log.stdout = log.stdout.replace(&token, \"<TOKEN>\");\n    log.stderr = log.stderr.replace(&token, \"<TOKEN>\");\n  }\n\n  res.logs.push(log);\n\n  if !all_logs_success(&res.logs) {\n    return Ok(res);\n  }\n\n  if let Some(commit) = args.commit {\n    let reset_log = run_komodo_command(\n      \"set commit\",\n      res.path.as_path(),\n      format!(\"git reset --hard {commit}\",),\n    )\n    .await;\n    res.logs.push(reset_log);\n  }\n\n  if !all_logs_success(&res.logs) {\n    return Ok(res);\n  }\n\n  match get_commit_hash_log(&res.path)\n    .await\n    .context(\"Failed to get latest commit\")\n  {\n    Ok((log, hash, message)) => {\n      res.logs.push(log);\n      res.commit_hash = Some(hash);\n      res.commit_message = Some(message);\n    }\n    Err(e) => {\n      res\n        .logs\n        .push(Log::simple(\"Latest Commit\", format_serror(&e.into())));\n    }\n  };\n\n  Ok(res)\n}\n"
  },
  {
    "path": "lib/git/src/commit.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse anyhow::Context;\nuse command::run_komodo_command;\nuse formatting::format_serror;\nuse komodo_client::entities::{\n  RepoExecutionResponse, all_logs_success, update::Log,\n};\nuse run_command::async_run_command;\nuse tokio::fs;\n\nuse crate::get_commit_hash_log;\n\n/// Write file, add, commit, force push.\n/// Repo must be cloned.\npub async fn write_commit_file(\n  commit_msg: &str,\n  repo_dir: &Path,\n  // relative to repo root\n  relative_file_path: &Path,\n  contents: &str,\n  branch: &str,\n) -> anyhow::Result<RepoExecutionResponse> {\n  let mut res = RepoExecutionResponse {\n    path: repo_dir.to_path_buf(),\n    logs: Vec::new(),\n    commit_hash: None,\n    commit_message: None,\n  };\n\n  // Clean up the path by stripping any redundant `/./`\n  let full_file_path = repo_dir\n    .join(relative_file_path)\n    .components()\n    .collect::<PathBuf>();\n\n  if let Some(parent) = full_file_path.parent() {\n    fs::create_dir_all(parent).await.with_context(|| {\n      format!(\"Failed to initialize file parent directory {parent:?}\")\n    })?;\n  }\n\n  fs::write(&full_file_path, contents)\n    .await\n    .with_context(|| {\n      format!(\"Failed to write contents to {full_file_path:?}\")\n    })?;\n\n  res.logs.push(Log::simple(\n    \"Write file\",\n    format!(\"File contents written to {full_file_path:?}\"),\n  ));\n\n  commit_file_inner(\n    commit_msg,\n    &mut res,\n    repo_dir,\n    relative_file_path,\n    branch,\n  )\n  .await;\n\n  Ok(res)\n}\n\n/// Add file, commit, force push.\n/// Repo must be cloned.\npub async fn commit_file(\n  commit_msg: &str,\n  repo_dir: &Path,\n  // relative to repo root\n  file: &Path,\n  branch: &str,\n) -> RepoExecutionResponse {\n  let mut res = RepoExecutionResponse {\n    path: repo_dir.to_path_buf(),\n    logs: Vec::new(),\n    commit_hash: None,\n    commit_message: None,\n  };\n\n  commit_file_inner(commit_msg, &mut res, repo_dir, file, branch)\n    .await;\n\n  res\n}\n\npub async fn commit_file_inner(\n  commit_msg: &str,\n  res: &mut RepoExecutionResponse,\n  repo_dir: &Path,\n  // relative to repo root\n  file: &Path,\n  branch: &str,\n) {\n  ensure_global_git_config_set().await;\n\n  let add_log = run_komodo_command(\n    \"Add Files\",\n    repo_dir,\n    format!(\"git add {}\", file.display()),\n  )\n  .await;\n  res.logs.push(add_log);\n  if !all_logs_success(&res.logs) {\n    return;\n  }\n\n  let commit_log = run_komodo_command(\n    \"Commit\",\n    repo_dir,\n    format!(\n      \"git commit -m \\\"[Komodo] {commit_msg}: update {file:?}\\\"\",\n    ),\n  )\n  .await;\n\n  if !commit_log.success {\n    // The user may have nothing to commit, but still should continue push the changes\n    if !commit_log.stdout.contains(\"nothing to commit\") {\n      res.logs.push(commit_log);\n      return;\n    }\n  } else {\n    res.logs.push(commit_log);\n  }\n\n  match get_commit_hash_log(repo_dir).await {\n    Ok((log, hash, message)) => {\n      res.logs.push(log);\n      res.commit_hash = Some(hash);\n      res.commit_message = Some(message);\n    }\n    Err(e) => {\n      res.logs.push(Log::error(\n        \"Get commit hash\",\n        format_serror(&e.into()),\n      ));\n      return;\n    }\n  };\n\n  let push_log = run_komodo_command(\n    \"Push\",\n    repo_dir,\n    format!(\"git push --set-upstream origin {branch}\"),\n  )\n  .await;\n\n  res.logs.push(push_log);\n}\n\n/// Add, commit, and force push.\n/// Repo must be cloned.\npub async fn commit_all(\n  repo_dir: &Path,\n  message: &str,\n  branch: &str,\n) -> RepoExecutionResponse {\n  ensure_global_git_config_set().await;\n\n  let mut res = RepoExecutionResponse {\n    path: repo_dir.to_path_buf(),\n    logs: Vec::new(),\n    commit_hash: None,\n    commit_message: None,\n  };\n\n  let add_log =\n    run_komodo_command(\"Add Files\", repo_dir, \"git add -A\").await;\n  res.logs.push(add_log);\n  if !all_logs_success(&res.logs) {\n    return res;\n  }\n\n  let commit_log = run_komodo_command(\n    \"Commit\",\n    repo_dir,\n    format!(\"git commit -m \\\"[Komodo] {message}\\\"\"),\n  )\n  .await;\n  res.logs.push(commit_log);\n  if !all_logs_success(&res.logs) {\n    return res;\n  }\n\n  match get_commit_hash_log(repo_dir).await {\n    Ok((log, hash, message)) => {\n      res.logs.push(log);\n      res.commit_hash = Some(hash);\n      res.commit_message = Some(message);\n    }\n    Err(e) => {\n      res.logs.push(Log::error(\n        \"Get commit hash\",\n        format_serror(&e.into()),\n      ));\n      return res;\n    }\n  };\n\n  let push_log = run_komodo_command(\n    \"Push\",\n    repo_dir,\n    format!(\"git push --set-upstream origin {branch}\"),\n  )\n  .await;\n  res.logs.push(push_log);\n\n  res\n}\n\nasync fn ensure_global_git_config_set() {\n  let res =\n    async_run_command(\"git config --global --get user.email\").await;\n  if !res.success() {\n    let _ = async_run_command(\n      \"git config --global user.email komodo@komo.do\",\n    )\n    .await;\n  }\n  let res =\n    async_run_command(\"git config --global --get user.name\").await;\n  if !res.success() {\n    let _ =\n      async_run_command(\"git config --global user.name komodo\").await;\n  }\n}\n"
  },
  {
    "path": "lib/git/src/init.rs",
    "content": "use std::path::Path;\n\nuse command::run_komodo_command;\nuse formatting::format_serror;\nuse komodo_client::entities::{\n  RepoExecutionArgs, all_logs_success, update::Log,\n};\n\npub async fn init_folder_as_repo(\n  folder_path: &Path,\n  args: &RepoExecutionArgs,\n  access_token: Option<&str>,\n  logs: &mut Vec<Log>,\n) {\n  // let folder_path = args.path(repo_dir);\n  // Initialize the folder as a git repo\n  let init_repo =\n    run_komodo_command(\"Git Init\", folder_path, \"git init\").await;\n  logs.push(init_repo);\n  if !all_logs_success(logs) {\n    return;\n  }\n\n  let repo_url = match args.remote_url(access_token) {\n    Ok(url) => url,\n    Err(e) => {\n      logs\n        .push(Log::error(\"Add git remote\", format_serror(&e.into())));\n      return;\n    }\n  };\n\n  // Set remote url\n  let mut set_remote = run_komodo_command(\n    \"Add git remote\",\n    folder_path,\n    format!(\"git remote add origin {repo_url}\"),\n  )\n  .await;\n  // Sanitize the output\n  if let Some(token) = &access_token {\n    set_remote.command = set_remote.command.replace(token, \"<TOKEN>\");\n    set_remote.stdout = set_remote.stdout.replace(token, \"<TOKEN>\");\n    set_remote.stderr = set_remote.stderr.replace(token, \"<TOKEN>\");\n  }\n  if !set_remote.success {\n    logs.push(set_remote);\n    return;\n  }\n\n  // Set branch.\n  let init_repo = run_komodo_command(\n    \"Set Branch\",\n    folder_path,\n    format!(\"git switch -c {}\", args.branch),\n  )\n  .await;\n  if !init_repo.success {\n    logs.push(init_repo);\n  }\n}\n"
  },
  {
    "path": "lib/git/src/lib.rs",
    "content": "use std::path::Path;\n\nuse anyhow::{Context, anyhow};\nuse formatting::{bold, muted};\nuse komodo_client::entities::{\n  LatestCommit, komodo_timestamp, update::Log,\n};\nuse run_command::async_run_command;\nuse tracing::instrument;\n\nmod clone;\nmod commit;\nmod init;\nmod pull;\nmod pull_or_clone;\n\npub use crate::{\n  clone::clone,\n  commit::{commit_all, commit_file, write_commit_file},\n  init::init_folder_as_repo,\n  pull::pull,\n  pull_or_clone::pull_or_clone,\n};\n\n#[instrument(level = \"debug\")]\npub async fn get_commit_hash_info(\n  repo_dir: &Path,\n) -> anyhow::Result<LatestCommit> {\n  let command = format!(\n    \"cd {} && git rev-parse --short HEAD && git rev-parse HEAD && git log -1 --pretty=%B\",\n    repo_dir.display()\n  );\n  let output = async_run_command(&command).await;\n  let mut split = output.stdout.split('\\n');\n  let (hash, _, message) = (\n    split\n      .next()\n      .context(\"Failed to get short commit hash\")?\n      .to_string(),\n    split.next().context(\"failed to get long commit hash\")?,\n    split\n      .next()\n      .context(\"Failed to get commit message\")?\n      .to_string(),\n  );\n  Ok(LatestCommit { hash, message })\n}\n/// returns (Log, commit hash, commit message)\n#[instrument(level = \"debug\")]\npub async fn get_commit_hash_log(\n  repo_dir: &Path,\n) -> anyhow::Result<(Log, String, String)> {\n  let start_ts = komodo_timestamp();\n  let command = format!(\n    \"cd {} && git rev-parse --short HEAD && git rev-parse HEAD && git log -1 --pretty=%B\",\n    repo_dir.display()\n  );\n  let output = async_run_command(&command).await;\n  let mut split = output.stdout.split('\\n');\n  let (short_hash, _, msg) = (\n    split\n      .next()\n      .context(\"Failed to get short commit hash\")?\n      .to_string(),\n    split.next().context(\"Failed to get long commit hash\")?,\n    split\n      .next()\n      .context(\"Failed to get commit message\")?\n      .to_string(),\n  );\n  let log = Log {\n    stage: \"Latest Commit\".into(),\n    command,\n    stdout: format!(\n      \"{} {}\\n{} {}\",\n      muted(\"hash:\"),\n      bold(&short_hash),\n      muted(\"message:\"),\n      bold(&msg),\n    ),\n    stderr: String::new(),\n    success: true,\n    start_ts,\n    end_ts: komodo_timestamp(),\n  };\n  Ok((log, short_hash, msg))\n}\n\n/// Gets the remote url, with `.git` stripped from the end.\npub async fn get_remote_url(path: &Path) -> anyhow::Result<String> {\n  let command =\n    format!(\"cd {} && git remote show origin\", path.display());\n  let output = async_run_command(&command).await;\n  if output.success() {\n    Ok(\n      output\n        .stdout\n        .strip_suffix(\".git\")\n        .map(str::to_string)\n        .unwrap_or(output.stdout),\n    )\n  } else {\n    Err(anyhow!(\n      \"Failed to get remote url | stdout: {} | stderr: {}\",\n      output.stdout,\n      output.stderr\n    ))\n  }\n}\n"
  },
  {
    "path": "lib/git/src/pull.rs",
    "content": "use std::{\n  path::{Path, PathBuf},\n  sync::OnceLock,\n};\n\nuse cache::TimeoutCache;\nuse command::run_komodo_command;\nuse formatting::format_serror;\nuse komodo_client::entities::{\n  RepoExecutionArgs, RepoExecutionResponse, all_logs_success,\n  komodo_timestamp, update::Log,\n};\n\nuse crate::get_commit_hash_log;\n\n/// Wait this long after a pull to allow another pull through\nconst PULL_TIMEOUT: i64 = 5_000;\n\nfn pull_cache()\n-> &'static TimeoutCache<PathBuf, RepoExecutionResponse> {\n  static PULL_CACHE: OnceLock<\n    TimeoutCache<PathBuf, RepoExecutionResponse>,\n  > = OnceLock::new();\n  PULL_CACHE.get_or_init(Default::default)\n}\n\n/// This will pull in a way that handles edge cases\n/// from possible state of the repo. For example, the user\n/// can change branch after clone, or even the remote.\n#[tracing::instrument(\n  level = \"debug\",\n  skip(clone_args, access_token)\n)]\n#[allow(clippy::too_many_arguments)]\npub async fn pull<T>(\n  clone_args: T,\n  root_repo_dir: &Path,\n  access_token: Option<String>,\n) -> anyhow::Result<RepoExecutionResponse>\nwhere\n  T: Into<RepoExecutionArgs> + std::fmt::Debug,\n{\n  let args: RepoExecutionArgs = clone_args.into();\n  let repo_url = args.remote_url(access_token.as_deref())?;\n\n  let mut res = RepoExecutionResponse {\n    path: args.path(root_repo_dir),\n    logs: Vec::new(),\n    commit_hash: None,\n    commit_message: None,\n  };\n\n  // Acquire the path lock\n  let lock = pull_cache().get_lock(res.path.clone()).await;\n\n  // Lock the path lock, prevents simultaneous pulls by\n  // ensuring simultaneous pulls will wait for first to finish\n  // and checking cached results.\n  let mut locked = lock.lock().await;\n\n  // Early return from cache if lasted pulled with PULL_TIMEOUT\n  if locked.last_ts + PULL_TIMEOUT > komodo_timestamp() {\n    return locked.clone_res();\n  }\n\n  let res = async {\n    // Check for '.git' path to see if the folder is initialized as a git repo\n    let dot_git_path = res.path.join(\".git\");\n    if !dot_git_path.exists() {\n      crate::init::init_folder_as_repo(\n        &res.path,\n        &args,\n        access_token.as_deref(),\n        &mut res.logs,\n      )\n      .await;\n      if !all_logs_success(&res.logs) {\n        return Ok(res);\n      }\n    }\n\n    // Set remote url\n    let mut set_remote = run_komodo_command(\n      \"Set Git Remote\",\n      res.path.as_ref(),\n      format!(\"git remote set-url origin {repo_url}\"),\n    )\n    .await;\n    // Sanitize the output\n    if let Some(token) = access_token {\n      set_remote.command =\n        set_remote.command.replace(&token, \"<TOKEN>\");\n      set_remote.stdout =\n        set_remote.stdout.replace(&token, \"<TOKEN>\");\n      set_remote.stderr =\n        set_remote.stderr.replace(&token, \"<TOKEN>\");\n    }\n    res.logs.push(set_remote);\n    if !all_logs_success(&res.logs) {\n      return Ok(res);\n    }\n\n    // First fetch remote branches before checkout\n    let fetch = run_komodo_command(\n      \"Git Fetch\",\n      res.path.as_ref(),\n      \"git fetch --all --prune\",\n    )\n    .await;\n    if !fetch.success {\n      res.logs.push(fetch);\n      return Ok(res);\n    }\n\n    let checkout = run_komodo_command(\n      \"Checkout branch\",\n      res.path.as_ref(),\n      format!(\"git checkout -f {}\", args.branch),\n    )\n    .await;\n    res.logs.push(checkout);\n    if !all_logs_success(&res.logs) {\n      return Ok(res);\n    }\n\n    let pull_log = run_komodo_command(\n      \"Git pull\",\n      res.path.as_ref(),\n      format!(\"git pull --rebase --force origin {}\", args.branch),\n    )\n    .await;\n    res.logs.push(pull_log);\n    if !all_logs_success(&res.logs) {\n      return Ok(res);\n    }\n\n    if let Some(commit) = args.commit {\n      let reset_log = run_komodo_command(\n        \"Set commit\",\n        res.path.as_ref(),\n        format!(\"git reset --hard {commit}\"),\n      )\n      .await;\n      res.logs.push(reset_log);\n      if !all_logs_success(&res.logs) {\n        return Ok(res);\n      }\n    }\n\n    match get_commit_hash_log(&res.path).await {\n      Ok((log, hash, message)) => {\n        res.logs.push(log);\n        res.commit_hash = Some(hash);\n        res.commit_message = Some(message);\n      }\n      Err(e) => {\n        res.logs.push(Log::simple(\n          \"Latest Commit\",\n          format_serror(\n            &e.context(\"Failed to get latest commit\").into(),\n          ),\n        ));\n      }\n    };\n\n    anyhow::Ok(res)\n  }\n  .await;\n\n  // Set the cache with results. Any other calls waiting on the lock will\n  // then immediately also use this same result.\n  locked.set(&res, komodo_timestamp());\n\n  res\n}\n"
  },
  {
    "path": "lib/git/src/pull_or_clone.rs",
    "content": "use std::path::Path;\n\nuse komodo_client::entities::{\n  RepoExecutionArgs, RepoExecutionResponse,\n};\n\n/// This is a mix of clone / pull.\n///   - If the folder doesn't exist, it will clone the repo.\n///     - Second variable in tuple will be `true`\n///   - If it does, it will ensure the remote is correct,\n///     ensure the correct branch is (force) checked out,\n///     force pull the repo, and switch to specified hash if provided.\n#[tracing::instrument(\n  level = \"debug\",\n  skip(clone_args, access_token)\n)]\npub async fn pull_or_clone<T>(\n  clone_args: T,\n  root_repo_dir: &Path,\n  access_token: Option<String>,\n) -> anyhow::Result<(RepoExecutionResponse, bool)>\nwhere\n  T: Into<RepoExecutionArgs> + std::fmt::Debug,\n{\n  let args: RepoExecutionArgs = clone_args.into();\n  let folder_path = args.path(root_repo_dir);\n\n  if folder_path.exists() {\n    crate::pull(args, root_repo_dir, access_token)\n      .await\n      .map(|r| (r, false))\n  } else {\n    crate::clone(args, root_repo_dir, access_token)\n      .await\n      .map(|r| (r, true))\n  }\n}\n"
  },
  {
    "path": "lib/interpolate/Cargo.toml",
    "content": "[package]\nname = \"interpolate\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[dependencies]\nkomodo_client.workspace = true\n#\nsvi.workspace = true\n#\nanyhow.workspace = true"
  },
  {
    "path": "lib/interpolate/src/lib.rs",
    "content": "use std::collections::{HashMap, HashSet};\n\nuse anyhow::Context;\nuse komodo_client::entities::{\n  EnvironmentVar, build::Build, deployment::Deployment, repo::Repo,\n  stack::Stack, update::Log,\n};\n\npub struct Interpolator<'a> {\n  variables: Option<&'a HashMap<String, String>>,\n  secrets: &'a HashMap<String, String>,\n  variable_replacers: HashSet<(String, String)>,\n  pub secret_replacers: HashSet<(String, String)>,\n}\n\nimpl<'a> Interpolator<'a> {\n  pub fn new(\n    variables: Option<&'a HashMap<String, String>>,\n    secrets: &'a HashMap<String, String>,\n  ) -> Interpolator<'a> {\n    Interpolator {\n      variables,\n      secrets,\n      variable_replacers: Default::default(),\n      secret_replacers: Default::default(),\n    }\n  }\n\n  pub fn interpolate_stack(\n    &mut self,\n    stack: &mut Stack,\n  ) -> anyhow::Result<&mut Self> {\n    if stack.config.skip_secret_interp {\n      return Ok(self);\n    }\n    self\n      .interpolate_string(&mut stack.config.file_contents)?\n      .interpolate_string(&mut stack.config.environment)?\n      .interpolate_string(&mut stack.config.pre_deploy.command)?\n      .interpolate_string(&mut stack.config.post_deploy.command)?\n      .interpolate_extra_args(&mut stack.config.extra_args)?\n      .interpolate_extra_args(&mut stack.config.build_extra_args)\n  }\n\n  pub fn interpolate_repo(\n    &mut self,\n    repo: &mut Repo,\n  ) -> anyhow::Result<&mut Self> {\n    if repo.config.skip_secret_interp {\n      return Ok(self);\n    }\n    self\n      .interpolate_string(&mut repo.config.environment)?\n      .interpolate_string(&mut repo.config.on_clone.command)?\n      .interpolate_string(&mut repo.config.on_pull.command)\n  }\n\n  pub fn interpolate_build(\n    &mut self,\n    build: &mut Build,\n  ) -> anyhow::Result<&mut Self> {\n    if build.config.skip_secret_interp {\n      return Ok(self);\n    }\n    self\n      .interpolate_string(&mut build.config.build_args)?\n      .interpolate_string(&mut build.config.secret_args)?\n      .interpolate_string(&mut build.config.labels)?\n      .interpolate_string(&mut build.config.pre_build.command)?\n      .interpolate_string(&mut build.config.dockerfile)?\n      .interpolate_extra_args(&mut build.config.extra_args)\n  }\n\n  pub fn interpolate_deployment(\n    &mut self,\n    deployment: &mut Deployment,\n  ) -> anyhow::Result<&mut Self> {\n    if deployment.config.skip_secret_interp {\n      return Ok(self);\n    }\n    self\n      .interpolate_string(&mut deployment.config.environment)?\n      .interpolate_string(&mut deployment.config.ports)?\n      .interpolate_string(&mut deployment.config.volumes)?\n      .interpolate_string(&mut deployment.config.labels)?\n      .interpolate_string(&mut deployment.config.command)?\n      .interpolate_extra_args(&mut deployment.config.extra_args)\n  }\n\n  pub fn interpolate_string(\n    &mut self,\n    target: &mut String,\n  ) -> anyhow::Result<&mut Self> {\n    if target.is_empty() {\n      return Ok(self);\n    }\n\n    // first pass - variables\n    let res = if let Some(variables) = self.variables {\n      let (res, more_replacers) = svi::interpolate_variables(\n        target,\n        variables,\n        svi::Interpolator::DoubleBrackets,\n        false,\n      )\n      .with_context(|| {\n        format!(\n          \"failed to interpolate variables into target '{target}'\",\n        )\n      })?;\n      self.variable_replacers.extend(more_replacers);\n      res\n    } else {\n      target.to_string()\n    };\n\n    // second pass - secrets\n    let (res, more_replacers) = svi::interpolate_variables(\n      &res,\n      self.secrets,\n      svi::Interpolator::DoubleBrackets,\n      false,\n    )\n    .with_context(|| {\n      format!(\"failed to interpolate secrets into target '{target}'\",)\n    })?;\n    self.secret_replacers.extend(more_replacers);\n\n    // Set with result\n    *target = res;\n\n    Ok(self)\n  }\n\n  pub fn interpolate_extra_args(\n    &mut self,\n    extra_args: &mut Vec<String>,\n  ) -> anyhow::Result<&mut Self> {\n    for arg in extra_args {\n      self\n        .interpolate_string(arg)\n        .context(\"failed interpolation into extra arg\")?;\n    }\n    Ok(self)\n  }\n\n  pub fn interpolate_env_vars(\n    &mut self,\n    env_vars: &mut Vec<EnvironmentVar>,\n  ) -> anyhow::Result<&mut Self> {\n    for var in env_vars {\n      self\n        .interpolate_string(&mut var.value)\n        .context(\"failed interpolation into variable value\")?;\n    }\n    Ok(self)\n  }\n\n  pub fn push_logs(&self, logs: &mut Vec<Log>) {\n    // Show which variables / values were interpolated\n    if !self.variable_replacers.is_empty() {\n      logs.push(Log::simple(\"Interpolate Variables\", self.variable_replacers\n        .iter()\n        .map(|(value, variable)| format!(\"<span class=\\\"text-muted-foreground\\\">{variable} =></span> {value}\"))\n        .collect::<Vec<_>>()\n        .join(\"\\n\")));\n    }\n\n    // Only show names of interpolated secrets\n    if !self.secret_replacers.is_empty() {\n      logs.push(\n        Log::simple(\"Interpolate Secrets\",\n        self.secret_replacers\n          .iter()\n          .map(|(_, variable)| format!(\"<span class=\\\"text-muted-foreground\\\">replaced:</span> {variable}\"))\n          .collect::<Vec<_>>()\n          .join(\"\\n\"),)\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "lib/logger/Cargo.toml",
    "content": "[package]\nname = \"logger\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n# local client\nkomodo_client.workspace = true\n# external\nanyhow.workspace = true\ntracing.workspace = true\nopentelemetry.workspace = true\nopentelemetry_sdk.workspace = true\nopentelemetry-otlp.workspace = true\ntracing-subscriber.workspace = true\ntracing-opentelemetry.workspace = true\nopentelemetry-semantic-conventions.workspace = true"
  },
  {
    "path": "lib/logger/README.md",
    "content": "# Logger module\n\nHelpers to configure standardized application logging / opentelemetry output."
  },
  {
    "path": "lib/logger/src/lib.rs",
    "content": "use anyhow::Context;\nuse komodo_client::entities::logger::{LogConfig, StdioLogMode};\nuse tracing::level_filters::LevelFilter;\nuse tracing_opentelemetry::OpenTelemetryLayer;\nuse tracing_subscriber::{\n  Registry, layer::SubscriberExt, util::SubscriberInitExt,\n};\n\nmod otel;\n\npub fn init(config: &LogConfig) -> anyhow::Result<()> {\n  let log_level: tracing::Level = config.level.into();\n\n  let registry =\n    Registry::default().with(LevelFilter::from(log_level));\n\n  let use_otel = !config.otlp_endpoint.is_empty();\n\n  match (config.stdio, use_otel, config.pretty) {\n    (StdioLogMode::Standard, true, true) => {\n      let tracer = otel::tracer(\n        &config.otlp_endpoint,\n        config.opentelemetry_service_name.clone(),\n      );\n      registry\n        .with(\n          tracing_subscriber::fmt::layer()\n            .pretty()\n            .with_file(false)\n            .with_line_number(false)\n            .with_target(config.location),\n        )\n        .with(OpenTelemetryLayer::new(tracer))\n        .try_init()\n    }\n    (StdioLogMode::Standard, true, false) => {\n      let tracer = otel::tracer(\n        &config.otlp_endpoint,\n        config.opentelemetry_service_name.clone(),\n      );\n      registry\n        .with(\n          tracing_subscriber::fmt::layer()\n            .with_file(false)\n            .with_line_number(false)\n            .with_target(config.location),\n        )\n        .with(OpenTelemetryLayer::new(tracer))\n        .try_init()\n    }\n\n    (StdioLogMode::Json, true, _) => {\n      let tracer = otel::tracer(\n        &config.otlp_endpoint,\n        config.opentelemetry_service_name.clone(),\n      );\n      registry\n        .with(tracing_subscriber::fmt::layer().json())\n        .with(OpenTelemetryLayer::new(tracer))\n        .try_init()\n    }\n\n    (StdioLogMode::Standard, false, true) => registry\n      .with(\n        tracing_subscriber::fmt::layer()\n          .pretty()\n          .with_file(false)\n          .with_line_number(false)\n          .with_target(config.location),\n      )\n      .try_init(),\n    (StdioLogMode::Standard, false, false) => registry\n      .with(\n        tracing_subscriber::fmt::layer()\n          .with_file(false)\n          .with_line_number(false)\n          .with_target(config.location),\n      )\n      .try_init(),\n\n    (StdioLogMode::Json, false, _) => registry\n      .with(tracing_subscriber::fmt::layer().json())\n      .try_init(),\n\n    (StdioLogMode::None, true, _) => {\n      let tracer = otel::tracer(\n        &config.otlp_endpoint,\n        config.opentelemetry_service_name.clone(),\n      );\n      registry.with(OpenTelemetryLayer::new(tracer)).try_init()\n    }\n    (StdioLogMode::None, false, _) => Ok(()),\n  }\n  .context(\"failed to init logger\")\n}\n"
  },
  {
    "path": "lib/logger/src/otel.rs",
    "content": "use std::time::Duration;\n\nuse opentelemetry::{KeyValue, global, trace::TracerProvider};\nuse opentelemetry_otlp::WithExportConfig;\nuse opentelemetry_sdk::{\n  Resource,\n  trace::{Sampler, Tracer},\n};\nuse opentelemetry_semantic_conventions::resource::SERVICE_VERSION;\n\nfn resource(service_name: String) -> Resource {\n  Resource::builder()\n    .with_service_name(service_name)\n    .with_attribute(KeyValue::new(\n      SERVICE_VERSION,\n      env!(\"CARGO_PKG_VERSION\"),\n    ))\n    .build()\n}\n\npub fn tracer(endpoint: &str, service_name: String) -> Tracer {\n  let provider =\n    opentelemetry_sdk::trace::TracerProviderBuilder::default()\n      .with_resource(resource(service_name.clone()))\n      .with_sampler(Sampler::AlwaysOn)\n      .with_batch_exporter(\n        opentelemetry_otlp::SpanExporter::builder()\n          .with_http()\n          .with_endpoint(endpoint)\n          .with_timeout(Duration::from_secs(3))\n          .build()\n          .unwrap(),\n      )\n      .build();\n  global::set_tracer_provider(provider.clone());\n  provider.tracer(service_name)\n}\n"
  },
  {
    "path": "lib/response/Cargo.toml",
    "content": "[package]\nname = \"response\"\nversion.workspace = true\nedition.workspace = true\nauthors.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\n\n[dependencies]\nserde_json.workspace = true\nserror.workspace = true\nanyhow.workspace = true\nserde.workspace = true\naxum.workspace = true"
  },
  {
    "path": "lib/response/src/lib.rs",
    "content": "use anyhow::Context;\nuse axum::http::{HeaderValue, StatusCode, header::CONTENT_TYPE};\nuse serde::Serialize;\nuse serror::serialize_error;\n\npub struct Response(pub axum::response::Response);\n\nimpl<T> From<T> for Response\nwhere\n  T: Serialize,\n{\n  fn from(value: T) -> Response {\n    let res = match serde_json::to_string(&value)\n      .context(\"failed to serialize response body\")\n    {\n      Ok(body) => axum::response::Response::builder()\n        .header(\n          CONTENT_TYPE,\n          HeaderValue::from_static(\"application/json\"),\n        )\n        .body(axum::body::Body::from(body))\n        .unwrap(),\n      Err(e) => axum::response::Response::builder()\n        .status(StatusCode::INTERNAL_SERVER_ERROR)\n        .header(\n          CONTENT_TYPE,\n          HeaderValue::from_static(\"application/json\"),\n        )\n        .body(axum::body::Body::from(serialize_error(&e)))\n        .unwrap(),\n    };\n    Response(res)\n  }\n}\n\npub enum JsonString {\n  Ok(String),\n  Err(serde_json::Error),\n}\n\nimpl<T> From<T> for JsonString\nwhere\n  T: Serialize,\n{\n  fn from(value: T) -> JsonString {\n    match serde_json::to_string(&value) {\n      Ok(body) => JsonString::Ok(body),\n      Err(e) => JsonString::Err(e),\n    }\n  }\n}\n\nimpl JsonString {\n  pub fn into_response(self) -> axum::response::Response {\n    match self {\n      JsonString::Ok(body) => axum::response::Response::builder()\n        .header(\n          CONTENT_TYPE,\n          HeaderValue::from_static(\"application/json\"),\n        )\n        .body(axum::body::Body::from(body))\n        .unwrap(),\n      JsonString::Err(error) => axum::response::Response::builder()\n        .status(StatusCode::INTERNAL_SERVER_ERROR)\n        .header(\n          CONTENT_TYPE,\n          HeaderValue::from_static(\"application/json\"),\n        )\n        .body(axum::body::Body::from(serialize_error(\n          &anyhow::Error::from(error)\n            .context(\"failed to serialize response body\"),\n        )))\n        .unwrap(),\n    }\n  }\n}\n"
  },
  {
    "path": "readme.md",
    "content": "# Komodo 🦎\n\nA tool to build and deploy software across many servers. \n\n🦎 [See the docs](https://komo.do)\n\n🦎 [Try the Demo](https://demo.komo.do) - Login: `demo` : `demo`\n\n🦎 [See the Build Server](https://build.komo.do)  - Login: `komodo` : `komodo`\n\n🦎 [Join the Discord](https://discord.gg/DRqE8Fvg5c)\n\n## About\n\nThe Komodo dragon is the largest living member of the [*Monitor* family of lizards](https://en.wikipedia.org/wiki/Monitor_lizard).\n\nThere is no limit to the number of servers you can connect, and there will never be. There is no limit to what API you can use for automation, and there never will be. No \"business edition\" here.\n\n## Disclaimer\n\nWarning. This is open source software (GPL-V3), and while we make a best effort to ensure releases are stable and bug-free,\nthere are no warranties. Use at your own risk.\n\n## Links\n\n- [periphery setup](https://github.com/moghtech/komodo/blob/main/scripts/readme.md)\n- [roadmap](https://github.com/moghtech/komodo/blob/main/roadmap.md)\n\n## Screenshots\n\n### Light Theme\n\n![Dashboard](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Light-Dashboard.png)\n![Stack](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Light-Stack.png)\n![Compose](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Light-Compose.png)\n![Env](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Light-Env.png)\n![Sync](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Light-Sync.png)\n![Update](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Light-Update.png)\n![Stats](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Light-Stats.png)\n![Export](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Light-Export.png)\n\n### Dark Theme\n\n![Dashboard](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Dark-Dashboard.png)\n![Stack](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Dark-Stack.png)\n![Compose](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Dark-Compose.png)\n![Env](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Dark-Env.png)\n![Sync](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Dark-Sync.png)\n![Update](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Dark-Update.png)\n![Stats](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Dark-Stats.png)\n![Export](https://raw.githubusercontent.com/moghtech/komodo/main/screenshots/Dark-Export.png)\n"
  },
  {
    "path": "roadmap.md",
    "content": "# Roadmap\n\nIn order to clarify the goals and invite community participation in the direction of the project, this document will serve as a roadmap for upcoming features / releases.\n\nIf you have an idea for Komodo, feel free to open an issue beginning with the `[Request]` tag. The community is also encouraged to open PRs fulfilling the goals of any planned release.\n\n## Release plans\n\n- **v1.12**: Support any git provider / docker registry (supports self-hosted providers like Gitea) ✅\n- **v1.13**: Support \"Compose\" resource - Paste in a docker compose file and manage it like a Portainer \"Stack\" ✅\n- **v1.14**: Manage docker networks, images, volumes in the UI ✅\n- **v1.15**: Support generic OIDC providers (including self-hosted) ✅\n- **v1.16**: \"Action\" resource: Run requests on the Komodo API using snippets of typescript. ✅\n- **v1.17**: Procedure Schedules: Run procedures / Actions at scheduled times, like CRON job. Connect to host terminals and exec into containers ✅\n- **v1.18**: Upgrade granular role based access control system ✅\n- **Undecided**: Support \"Swarm\" resource - Manage docker swarms, attach Deployments / Stacks to \"Swarm\". \n- **Undecided**: Support \"Cluster\" resource - Manage Kubernetes cluster, can attach deployments to \"Cluster\"\n\n**Note. The specific versions associated with these features are not final.**"
  },
  {
    "path": "runfile.toml",
    "content": "# Runfile | https://crates.io/crates/runnables-cli\n\n[dev-core]\nalias = \"dc\"\ndescription = \"runs core --release pointing to .dev/core.config.toml\"\ncmd = \"KOMODO_CONFIG_PATH=.dev/core.config.toml cargo run -p komodo_core --release\"\n\n[dev-periphery]\nalias = \"dp\"\ndescription = \"runs periphery --release pointing to .dev/periphery.config.toml\"\ncmd = \"PERIPHERY_CONFIG_PATH=.dev/periphery.config.toml cargo run -p komodo_periphery --release\"\n\n[yarn-install]\ndescription = \"downloads latest javacript dependencies for client and frontend\"\ncmd = \"\"\"\ncd frontend && yarn && \\\ncd ../docsite && yarn && \\\ncd ../client/core/ts && yarn\n\"\"\"\n\n[gen-client]\nalias = \"gc\"\ndescription = \"generates typescript types and build the ts client\"\nafter = \"yarn-install\"\ncmd = \"\"\"\nnode ./client/core/ts/generate_types.mjs && \\\ncd ./client/core/ts && yarn build && \\\ncp -r dist/. ../../../frontend/public/client/.\"\"\"\n\n[link-client]\ndescription = \"yarn links the ts client to the frontend\"\nafter = \"gen-client\"\ncmd = \"\"\"\ncd ./client/core/ts && yarn link && \\\ncd ../../../frontend && yarn link komodo_client && yarn\"\"\"\n\n[dev-compose]\ndescription = \"deploys dev.compose.yaml\"\ncmd = \"\"\"\ndocker compose -p komodo-dev -f dev.compose.yaml down --remove-orphans && \\\ndocker compose -p komodo-dev -f dev.compose.yaml up -d\"\"\"\n\n[dev-compose-exposed]\ndescription = \"deploys dev.compose.yaml with exposed port and non-ssl periphery\"\ncmd = \"\"\"\ndocker compose -p komodo-dev down --remove-orphans && \\\ndocker compose -p komodo-dev -f dev.compose.yaml -f expose.compose.yaml up -d\"\"\"\n\n[dev-compose-build]\ndescription = \"builds and deploys dev.compose.yaml\"\ncmd = \"docker compose -p komodo-dev -f dev.compose.yaml build\"\n\n[dev-rustdoc]\ndescription = \"starts the rustdoc site (https://docs.rs/komodo_client/latest/komodo_client/) in dev mode\"\ncmd = \"cargo doc --no-deps -p komodo_client && http-server -p 8050 target/doc\"\n\n[deploy-komodo]\nalias = \"dk\"\ncmd = \"deno run --allow-all deploy/komodo.ts\""
  },
  {
    "path": "rustfmt.toml",
    "content": "max_width = 70\ntab_spaces = 2"
  },
  {
    "path": "scripts/install-cli.py",
    "content": "import sys\nimport os\nimport platform\nimport json\nimport urllib.request\n\ndef load_version():\n\tversion = \"\"\n\tfor arg in sys.argv:\n\t\tif arg.count(\"--version\") > 0:\n\t\t\tversion = arg.split(\"=\")[1]\n\tif len(version) == 0:\n\t\tversion = load_latest_version()\n\treturn version\n\ndef load_latest_version():\n\treturn json.load(urllib.request.urlopen(\"https://api.github.com/repos/moghtech/komodo/releases/latest\"))[\"tag_name\"]\n\ndef load_bin_dir():\n\thome_dir = os.environ['HOME']\n\t# Checks if setup.py is passed --user arg\n\tuser_install = sys.argv.count(\"--user\") > 0\n\tif user_install:\n\t\treturn f'{home_dir}/.local/bin'\n\telse:\n\t\treturn \"/usr/local/bin\"\n\ndef copy_binary(bin_dir, version):\n\t# ensure bin_dir exists\n\tif not os.path.isdir(bin_dir):\n\t\tos.makedirs(bin_dir)\n\n\t# delete binary if it already exists\n\tbin_path = f'{bin_dir}/km'\n\tif os.path.isfile(bin_path):\n\t\tos.remove(bin_path)\n\n\tkm_bin = \"km-x86_64\"\n\tarch = platform.machine().lower()\n\tif arch == \"aarch64\" or arch == \"arm64\":\n\t\tprint(\"aarch64 detected\")\n\t\tkm_bin = \"km-aarch64\"\n\telse:\n\t\tprint(\"using x86_64 binary\")\n\n\t# download the binary to bin path\n\tprint(os.popen(f'curl -sSL https://github.com/moghtech/komodo/releases/download/{version}/{km_bin} > {bin_path}').read())\n\n\t# add executable permissions\n\tos.popen(f'chmod +x {bin_path}')\n\t\ndef main():\n\tprint(\"======================\")\n\tprint(\" KOMODO CLI INSTALLER \")\n\tprint(\"======================\")\n\n\tversion = load_version()\n\tbin_dir = load_bin_dir()\n \n\tprint(f'version: {version}')\n\tprint(f'install to: {bin_dir}/km')\n\n\tcopy_binary(bin_dir, version)\n\n\tprint(\"Finished komodo-cli setup. Try running 'km --help'.\\n\")\n\nmain()\n"
  },
  {
    "path": "scripts/readme.md",
    "content": "# Periphery setup script\n\nThese scripts will set up Komodo Periphery on your hosts, managed by systemd.\n\n*Note*. This script can be **run multiple times without issue**, and it won't change existing config after the first run. Just run it again after a Komodo version release, and it will update the periphery version.\n\n*Note*. The script can usually detect aarch64 system and use the periphery-aarch64 binary.\n\nThere's two ways to install periphery: `System` and `User`\n\n## System (requires root)\n\nNote. Run this after switching to root user (eg `sudo su -`).\n\n```sh\ncurl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | python3\n```\n\nWill install to paths:\n- periphery (binary) -> `/usr/local/bin/periphery`\n- periphery.service -> `/etc/systemd/system/periphery.service`\n- periphery.config.toml -> `/etc/komodo/periphery.config.toml`\n\n## User\n\n*Note*. The user running periphery must be a member of the docker group, in order to use the docker cli without sudo.\n\n```sh\ncurl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | python3 - --user\n```\n\nWill install to paths:\n- periphery (binary) -> `$HOME/.local/bin`\n- periphery.service -> `$HOME/.config/systemd/user/periphery.service`\n- periphery.config.toml -> `$HOME/.config/komodo/periphery.config.toml`\n\n*Note*. Ensure the user running periphery has write permissions to the configured folders `repo_dir`, `stack_dir`, and `ssl_key_file` / `ssl_cert_file` parent folder.\nThis allows periphery to clone repos, write compose files, and generate ssl certs.\n\n*Note*. To ensure periphery stays running when your user logs out, use `sudo loginctl enable-linger $USER`.\n\nFor example in `periphery.config.toml`, running under `ubuntu` user:\n```toml\nrepo_dir = \"/home/ubuntu/.komodo/repos\"\nstack_dir = \"/home/ubuntu/.komodo/stacks\"\n\nssl_enabled = true\nssl_key_file = \"/home/ubuntu/.komodo/ssl/key.pem\"\nssl_cert_file = \"/home/ubuntu/.komodo/ssl/cert.pem\"\n```\n\nFor additional information on configuring the systemd service, see the systemd service file documentation here:\n[https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html).\n\n## Force Service File Recreation\n\nUsually the installer will only create the systemd service files (`periphery.service`) if one doesn't already exist.\nThis means the user is free to customize it to fit their needs, such as changing the `User=` running the binary.\n\nYou can change this behavior by passing `--force-service-file`, which will restore the service file\nto the current default.\n\nExample:\n\n```sh\ncurl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | python3 - --force-service-file\n```"
  },
  {
    "path": "scripts/setup-periphery.py",
    "content": "import sys\nimport os\nimport shutil\nimport platform\nimport json\nimport urllib.request\n\ndef load_version():\n\tversion = \"\"\n\tfor arg in sys.argv:\n\t\tif arg.count(\"--version\") > 0:\n\t\t\tversion = arg.split(\"=\")[1]\n\tif len(version) == 0:\n\t\tversion = load_latest_version()\n\treturn version\n\ndef load_latest_version():\n\treturn json.load(urllib.request.urlopen(\"https://api.github.com/repos/moghtech/komodo/releases/latest\"))[\"tag_name\"]\n\ndef uses_systemd():\n\t# First check if systemctl is an available command, then check if systemd is the init system\n\treturn shutil.which(\"systemctl\") is not None and os.path.exists(\"/run/systemd/system/\")\n\ndef load_paths():\n\thome_dir = os.environ['HOME']\n\t# Checks if setup.py is passed --user arg\n\tuser_install = sys.argv.count(\"--user\") > 0\n\tif user_install:\n\t\treturn [\n\t\t\t# Is user install\n\t\t\tTrue,\n\t\t\t# home_dir\n\t\t\thome_dir,\n\t\t\t# binary location\n\t\t\tf'{home_dir}/.local/bin',\n\t\t\t# config location\n\t \t\tf'{home_dir}/.config/komodo',\n\t\t\t# service file location\n\t \t\tf'{home_dir}/.config/systemd/user',\n\t\t]\n\telse:\n\t\treturn [\n\t\t\t# Not user install\n\t\t\tFalse,\n\t\t\t# home_dir\n\t\t\thome_dir,\n\t\t\t# binary location\n\t\t\t\"/usr/local/bin\",\n\t\t\t# config location\n\t \t\t\"/etc/komodo\",\n\t\t\t# service file location\n\t \t\t\"/etc/systemd/system\",\n\t\t]\n\ndef copy_binary(user_install, bin_dir, version):\n\t# stop periphery in case its currently in use\n\tuser = \"\"\n\tif user_install:\n\t\tuser = \" --user\"\n\tos.popen(f'systemctl{user} stop periphery')\n\n\t# ensure bin_dir exists\n\tif not os.path.isdir(bin_dir):\n\t\tos.makedirs(bin_dir)\n\n\t# delete binary if it already exists\n\tbin_path = f'{bin_dir}/periphery'\n\tif os.path.isfile(bin_path):\n\t\tos.remove(bin_path)\n\n\tperiphery_bin = \"periphery-x86_64\"\n\tarch = platform.machine().lower()\n\tif arch == \"aarch64\" or arch == \"arm64\":\n\t\tprint(\"aarch64 detected\")\n\t\tperiphery_bin = \"periphery-aarch64\"\n\telse:\n\t\tprint(\"using x86_64 binary\")\n\n\t# download the binary to bin path\n\tprint(os.popen(f'curl -sSL https://github.com/moghtech/komodo/releases/download/{version}/{periphery_bin} > {bin_path}').read())\n\n\t# add executable permissions\n\tos.popen(f'chmod +x {bin_path}')\n\ndef copy_config(config_dir):\n\tconfig_file = f'{config_dir}/periphery.config.toml'\n\n\t# early return if config file already exists\n\tif os.path.isfile(config_file):\n\t\tprint(\"config already exists, skipping...\")\n\t\treturn\n\t\n\tprint(f'creating config at {config_file}')\n\n\t# ensure config dir exists\n\tif not os.path.isdir(config_dir):\n\t\tos.makedirs(config_dir)\n\n\tprint(os.popen(f'curl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/config/periphery.config.toml > {config_file}').read())\n\ndef copy_service_file(home_dir, bin_dir, config_dir, service_dir, user_install):\n\tservice_file = f'{service_dir}/periphery.service'\n\n\tforce_service_recopy = sys.argv.count(\"--force-service-file\") > 0\n\n\tif force_service_recopy:\n\t\tprint(\"forcing service file recreation\")\n\n\t# early return is service file already exists\n\tif os.path.isfile(service_file):\n\t\tif force_service_recopy:\n\t\t\tprint(\"deleting existing service file\")\n\t\t\tos.remove(service_file)\n\t\telse:\n\t\t\tprint(\"service file already exists, skipping...\")\n\t\t\treturn\n\t\n\tprint(f'creating service file at {service_file}')\n\t\n\t# ensure service_dir exists\n\tif not os.path.isdir(service_dir):\n\t\tos.makedirs(service_dir)\n\n\tf = open(service_file, \"x\")\n\tf.write((\n\t\t\"[Unit]\\n\"\n\t\t\"Description=Agent to connect with Komodo Core\\n\"\n\t\t\"\\n\"\n\t\t\"[Service]\\n\"\n\t\tf'Environment=\"HOME={home_dir}\"\\n'\n\t\tf'ExecStart=/bin/sh -lc \"{bin_dir}/periphery --config-path {config_dir}/periphery.config.toml\"\\n'\n\t\t\"Restart=on-failure\\n\"\n\t\t\"TimeoutStartSec=0\\n\"\n\t\t\"\\n\"\n\t\t\"[Install]\\n\"\n\t\t\"WantedBy=default.target\"\n\t))\n\n\tuser = \"\"\n\tif user_install:\n\t\tuser = \" --user\"\n\tos.popen(f'systemctl{user} daemon-reload')\n\t\ndef main():\n\tprint(\"=====================\")\n\tprint(\" PERIPHERY INSTALLER \")\n\tprint(\"=====================\")\n\n\tif not uses_systemd():\n\t\tprint(\"This installer requires systemd and systemd wasn't found. Exiting\")\n\t\tsys.exit(1)\n\n\tversion = load_version()\n\t[user_install, home_dir, bin_dir, config_dir, service_dir] = load_paths()\n \n\tprint(f'version: {version}')\n\tprint(f'user install: {user_install}')\n\tprint(f'home dir: {home_dir}')\n\tprint(f'bin dir: {bin_dir}')\n\tprint(f'config dir: {config_dir}')\n\tprint(f'service file dir: {service_dir}')\n\n\tforce_service_recopy = sys.argv.count(\"--force-service-file\") > 0\n\tif force_service_recopy:\n\t\tprint('forcing service file rewrite')\n\n\tcopy_binary(user_install, bin_dir, version)\n\tcopy_config(config_dir)\n\tcopy_service_file(home_dir, bin_dir, config_dir, service_dir, user_install)\n\n\tuser = \"\"\n\tif user_install:\n\t\tuser = \" --user\"\n\n\tprint(\"starting periphery...\")\n\tprint(os.popen(f'systemctl{user} start periphery').read())\n\n\tprint(\"Finished periphery setup.\\n\")\n\tprint(f'Note. Use \"systemctl{user} status periphery\" to make sure periphery is running')\n\tprint(f'Note. Use \"systemctl{user} enable periphery\" to have periphery start on system boot')\n\nmain()\n"
  },
  {
    "path": "typeshare.toml",
    "content": "[typescript.type_mappings]\n\"PathBuf\" = \"string\"\n\"u128\" = \"number\""
  }
]