diff --git a/.icons/airflow.svg b/.icons/airflow.svg new file mode 100644 index 0000000..46300fe --- /dev/null +++ b/.icons/airflow.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/.images/airflow.png b/.images/airflow.png new file mode 100644 index 0000000..bdd5798 Binary files /dev/null and b/.images/airflow.png differ diff --git a/apache-airflow/README.md b/apache-airflow/README.md new file mode 100644 index 0000000..bf8e315 --- /dev/null +++ b/apache-airflow/README.md @@ -0,0 +1,23 @@ +--- +display_name: airflow +description: A module that adds Apache Airflow in your Coder template +icon: ../.icons/airflow.svg +maintainer_github: coder +partner_github: nataindata +verified: true +tags: [airflow, idea, web, helper] +--- + +# airflow + +A module that adds Apache Airflow in your Coder template. + +```tf +module "airflow" { + source = "registry.coder.com/modules/apache-airflow/coder" + version = "1.0.13" + agent_id = coder_agent.example.id +} +``` + +![Airflow](../.images/airflow.png) diff --git a/apache-airflow/main.tf b/apache-airflow/main.tf new file mode 100644 index 0000000..91b6682 --- /dev/null +++ b/apache-airflow/main.tf @@ -0,0 +1,65 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.17" + } + } +} + +# Add required variables for your modules and remove any unneeded variables +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "log_path" { + type = string + description = "The path to log airflow to." + default = "/tmp/airflow.log" +} + +variable "port" { + type = number + description = "The port to run airflow on." + default = 8080 +} + +variable "share" { + type = string + default = "owner" + validation { + condition = var.share == "owner" || var.share == "authenticated" || var.share == "public" + error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'." + } +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +resource "coder_script" "airflow" { + agent_id = var.agent_id + display_name = "airflow" + icon = "/icon/apache-guacamole.svg" + script = templatefile("${path.module}/run.sh", { + LOG_PATH : var.log_path, + PORT : var.port + }) + run_on_start = true +} + +resource "coder_app" "airflow" { + agent_id = var.agent_id + slug = "airflow" + display_name = "airflow" + url = "http://localhost:${var.port}" + icon = "/icon/apache-guacamole.svg" + subdomain = true + share = var.share + order = var.order +} diff --git a/apache-airflow/run.sh b/apache-airflow/run.sh new file mode 100644 index 0000000..d881260 --- /dev/null +++ b/apache-airflow/run.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh + +BOLD='\033[0;1m' + +PATH=$PATH:~/.local/bin +pip install --upgrade apache-airflow + +filename=~/airflow/airflow.db +if ! [ -f $filename ] || ! [ -s $filename ]; then + airflow db init +fi + +export AIRFLOW__CORE__LOAD_EXAMPLES=false + +airflow webserver > ${LOG_PATH} 2>&1 & + +airflow scheduler >> /tmp/airflow_scheduler.log 2>&1 & + +airflow users create -u admin -p admin -r Admin -e admin@admin.com -f Coder -l User diff --git a/aws-region/README.md b/aws-region/README.md index 934714b..4d363c3 100644 --- a/aws-region/README.md +++ b/aws-region/README.md @@ -17,7 +17,7 @@ Customize the preselected parameter value: ```tf module "aws-region" { source = "registry.coder.com/modules/aws-region/coder" - version = "1.0.10" + version = "1.0.12" default = "us-east-1" } @@ -37,7 +37,7 @@ Change the display name and icon for a region using the corresponding maps: ```tf module "aws-region" { source = "registry.coder.com/modules/aws-region/coder" - version = "1.0.10" + version = "1.0.12" default = "ap-south-1" custom_names = { @@ -63,7 +63,7 @@ Hide the Asia Pacific regions Seoul and Osaka: ```tf module "aws-region" { source = "registry.coder.com/modules/aws-region/coder" - version = "1.0.10" + version = "1.0.12" exclude = ["ap-northeast-2", "ap-northeast-3"] } diff --git a/azure-region/README.md b/azure-region/README.md index d88a2e9..cd0efd3 100644 --- a/azure-region/README.md +++ b/azure-region/README.md @@ -14,7 +14,7 @@ This module adds a parameter with all Azure regions, allowing developers to sele ```tf module "azure_region" { source = "registry.coder.com/modules/azure-region/coder" - version = "1.0.2" + version = "1.0.12" default = "eastus" } @@ -34,7 +34,7 @@ Change the display name and icon for a region using the corresponding maps: ```tf module "azure-region" { source = "registry.coder.com/modules/azure-region/coder" - version = "1.0.2" + version = "1.0.12" custom_names = { "australia" : "Go Australia!" } @@ -57,7 +57,7 @@ Hide all regions in Australia except australiacentral: ```tf module "azure-region" { source = "registry.coder.com/modules/azure-region/coder" - version = "1.0.2" + version = "1.0.12" exclude = [ "australia", "australiacentral2", diff --git a/code-server/README.md b/code-server/README.md index 993b9ba..909c985 100644 --- a/code-server/README.md +++ b/code-server/README.md @@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.10" + version = "1.0.12" agent_id = coder_agent.example.id } ``` @@ -28,7 +28,7 @@ module "code-server" { ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.10" + version = "1.0.12" agent_id = coder_agent.example.id install_version = "4.8.3" } @@ -41,7 +41,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/): ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.10" + version = "1.0.12" agent_id = coder_agent.example.id extensions = [ "dracula-theme.theme-dracula" @@ -58,7 +58,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.10" + version = "1.0.12" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -74,7 +74,7 @@ Just run code-server in the background, don't fetch it from GitHub: ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.10" + version = "1.0.12" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] } @@ -89,7 +89,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub: ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.10" + version = "1.0.12" agent_id = coder_agent.example.id use_cached = true extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] @@ -101,7 +101,7 @@ Just run code-server in the background, don't fetch it from GitHub: ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.10" + version = "1.0.12" agent_id = coder_agent.example.id offline = true } diff --git a/code-server/main.tf b/code-server/main.tf index 8c0f1e0..c186c14 100644 --- a/code-server/main.tf +++ b/code-server/main.tf @@ -101,6 +101,12 @@ variable "extensions_dir" { default = "" } +variable "auto_install_extensions" { + type = bool + description = "Automatically install recommended extensions when code-server starts." + default = false +} + resource "coder_script" "code-server" { agent_id = var.agent_id display_name = "code-server" @@ -117,6 +123,8 @@ resource "coder_script" "code-server" { OFFLINE : var.offline, USE_CACHED : var.use_cached, EXTENSIONS_DIR : var.extensions_dir, + FOLDER : var.folder, + AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions, }) run_on_start = true diff --git a/code-server/run.sh b/code-server/run.sh index b04e131..26a1c6e 100755 --- a/code-server/run.sh +++ b/code-server/run.sh @@ -70,4 +70,24 @@ for extension in "$${EXTENSIONLIST[@]}"; do fi done +if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then + if ! command -v jq > /dev/null; then + echo "jq is required to install extensions from a workspace file." + exit 0 + fi + + WORKSPACE_DIR="$HOME" + if [ -n "${FOLDER}" ]; then + WORKSPACE_DIR="${FOLDER}" + fi + + if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then + printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR" + extensions=$(jq -r '.recommendations[]' "$WORKSPACE_DIR"/.vscode/extensions.json) + for extension in $extensions; do + $CODE_SERVER "$EXTENSION_ARG" --install-extension "$extension" + done + fi +fi + run_code_server diff --git a/dotfiles/README.md b/dotfiles/README.md index 9f8a0fe..be174c2 100644 --- a/dotfiles/README.md +++ b/dotfiles/README.md @@ -9,16 +9,61 @@ tags: [helper] # Dotfiles -Allow developers to optionally bring their own [dotfiles repository](https://dotfiles.github.io)! Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/latest/dotfiles) command. +Allow developers to optionally bring their own [dotfiles repository](https://dotfiles.github.io). + +This will prompt the user for their dotfiles repository URL on template creation using a `coder_parameter`. + +Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/latest/dotfiles) command. ```tf module "dotfiles" { source = "registry.coder.com/modules/dotfiles/coder" - version = "1.0.2" + version = "1.0.12" agent_id = coder_agent.example.id } ``` +## Examples + +### Apply dotfiles as the current user + +```tf +module "dotfiles" { + source = "registry.coder.com/modules/dotfiles/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +### Apply dotfiles as another user (only works if sudo is passwordless) + +```tf +module "dotfiles" { + source = "registry.coder.com/modules/dotfiles/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + user = "root" +} +``` + +### Apply the same dotfiles as the current user and root (the root dotfiles can only be applied if sudo is passwordless) + +```tf +module "dotfiles" { + source = "registry.coder.com/modules/dotfiles/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} + +module "dotfiles-root" { + source = "registry.coder.com/modules/dotfiles/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + user = "root" + dotfiles_uri = module.dotfiles.dotfiles_uri +} +``` + ## Setting a default dotfiles repository You can set a default dotfiles repository for all users by setting the `default_dotfiles_uri` variable: diff --git a/dotfiles/main.tf b/dotfiles/main.tf index ac7f7e6..950cb9b 100644 --- a/dotfiles/main.tf +++ b/dotfiles/main.tf @@ -16,10 +16,23 @@ variable "agent_id" { variable "default_dotfiles_uri" { type = string - description = "The default dotfiles URI if the workspace user does not provide one." + description = "The default dotfiles URI if the workspace user does not provide one" default = "" } +variable "dotfiles_uri" { + type = string + description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)" + + default = null +} + +variable "user" { + type = string + description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)" + default = null +} + variable "coder_parameter_order" { type = number description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." @@ -27,6 +40,8 @@ variable "coder_parameter_order" { } data "coder_parameter" "dotfiles_uri" { + count = var.dotfiles_uri == null ? 1 : 0 + type = "string" name = "dotfiles_uri" display_name = "Dotfiles URL (optional)" @@ -37,14 +52,17 @@ data "coder_parameter" "dotfiles_uri" { icon = "/icon/dotfiles.svg" } -resource "coder_script" "personalize" { - agent_id = var.agent_id - script = <<-EOT - DOTFILES_URI="${data.coder_parameter.dotfiles_uri.value}" - if [ -n "$${DOTFILES_URI// }" ]; then - coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.dotfiles.log - fi - EOT +locals { + dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value + user = var.user != null ? var.user : "" +} + +resource "coder_script" "dotfiles" { + agent_id = var.agent_id + script = templatefile("${path.module}/run.sh", { + DOTFILES_URI : local.dotfiles_uri, + DOTFILES_USER : local.user + }) display_name = "Dotfiles" icon = "/icon/dotfiles.svg" run_on_start = true @@ -52,5 +70,5 @@ resource "coder_script" "personalize" { output "dotfiles_uri" { description = "Dotfiles URI" - value = data.coder_parameter.dotfiles_uri.value + value = local.dotfiles_uri } diff --git a/dotfiles/run.sh b/dotfiles/run.sh new file mode 100644 index 0000000..9463439 --- /dev/null +++ b/dotfiles/run.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +DOTFILES_URI="${DOTFILES_URI}" +DOTFILES_USER="${DOTFILES_USER}" + +if [ -n "$${DOTFILES_URI// }" ]; then + if [ -z "$DOTFILES_USER" ]; then + DOTFILES_USER="$USER" + fi + + echo "✨ Applying dotfiles for user $DOTFILES_USER" + + if [ "$DOTFILES_USER" = "$USER" ]; then + coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log + else + # The `eval echo ~"$DOTFILES_USER"` part is used to dynamically get the home directory of the user, see https://superuser.com/a/484280 + # eval echo ~coder -> "/home/coder" + # eval echo ~root -> "/root" + + CODER_BIN=$(which coder) + DOTFILES_USER_HOME=$(eval echo ~"$DOTFILES_USER") + sudo -u "$DOTFILES_USER" sh -c "'$CODER_BIN' dotfiles '$DOTFILES_URI' -y 2>&1 | tee '$DOTFILES_USER_HOME'/.dotfiles.log" + fi +fi diff --git a/exoscale-instance-type/README.md b/exoscale-instance-type/README.md index 4b493f9..4296121 100644 --- a/exoscale-instance-type/README.md +++ b/exoscale-instance-type/README.md @@ -17,7 +17,7 @@ Customize the preselected parameter value: ```tf module "exoscale-instance-type" { source = "registry.coder.com/modules/exoscale-instance-type/coder" - version = "1.0.2" + version = "1.0.12" default = "standard.medium" } @@ -45,7 +45,7 @@ Change the display name a type using the corresponding maps: ```tf module "exoscale-instance-type" { source = "registry.coder.com/modules/exoscale-instance-type/coder" - version = "1.0.2" + version = "1.0.12" default = "standard.medium" custom_names = { @@ -79,7 +79,7 @@ Show only gpu1 types ```tf module "exoscale-instance-type" { source = "registry.coder.com/modules/exoscale-instance-type/coder" - version = "1.0.2" + version = "1.0.12" default = "gpu.large" type_category = ["gpu"] exclude = [ diff --git a/exoscale-zone/README.md b/exoscale-zone/README.md index 4297bed..0f4353e 100644 --- a/exoscale-zone/README.md +++ b/exoscale-zone/README.md @@ -17,7 +17,7 @@ Customize the preselected parameter value: ```tf module "exoscale-zone" { source = "registry.coder.com/modules/exoscale-zone/coder" - version = "1.0.2" + version = "1.0.12" default = "ch-dk-2" } @@ -44,7 +44,7 @@ Change the display name and icon for a zone using the corresponding maps: ```tf module "exoscale-zone" { source = "registry.coder.com/modules/exoscale-zone/coder" - version = "1.0.2" + version = "1.0.12" default = "at-vie-1" custom_names = { @@ -76,7 +76,7 @@ Hide the Switzerland zones Geneva and Zurich ```tf module "exoscale-zone" { source = "registry.coder.com/modules/exoscale-zone/coder" - version = "1.0.2" + version = "1.0.12" exclude = ["ch-gva-2", "ch-dk-2"] } diff --git a/gcp-region/README.md b/gcp-region/README.md index bb6063a..776d638 100644 --- a/gcp-region/README.md +++ b/gcp-region/README.md @@ -14,7 +14,7 @@ This module adds Google Cloud Platform regions to your Coder template. ```tf module "gcp_region" { source = "registry.coder.com/modules/gcp-region/coder" - version = "1.0.2" + version = "1.0.12" regions = ["us", "europe"] } @@ -34,7 +34,7 @@ Note: setting `gpu_only = true` and using a default region without GPU support, ```tf module "gcp_region" { source = "registry.coder.com/modules/gcp-region/coder" - version = "1.0.2" + version = "1.0.12" default = ["us-west1-a"] regions = ["us-west1"] gpu_only = false @@ -50,7 +50,7 @@ resource "google_compute_instance" "example" { ```tf module "gcp_region" { source = "registry.coder.com/modules/gcp-region/coder" - version = "1.0.2" + version = "1.0.12" regions = ["europe-west"] single_zone_per_region = false } @@ -65,7 +65,7 @@ resource "google_compute_instance" "example" { ```tf module "gcp_region" { source = "registry.coder.com/modules/gcp-region/coder" - version = "1.0.2" + version = "1.0.12" regions = ["us", "europe"] gpu_only = true single_zone_per_region = true diff --git a/git-clone/README.md b/git-clone/README.md index 054e30c..255b3f1 100644 --- a/git-clone/README.md +++ b/git-clone/README.md @@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it ```tf module "git-clone" { source = "registry.coder.com/modules/git-clone/coder" - version = "1.0.2" + version = "1.0.12" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" } @@ -27,7 +27,7 @@ module "git-clone" { ```tf module "git-clone" { source = "registry.coder.com/modules/git-clone/coder" - version = "1.0.2" + version = "1.0.12" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" base_dir = "~/projects/coder" @@ -41,7 +41,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov ```tf module "git-clone" { source = "registry.coder.com/modules/git-clone/coder" - version = "1.0.2" + version = "1.0.12" agent_id = coder_agent.example.id url = "https://github.com/coder/coder" } @@ -50,3 +50,106 @@ data "coder_git_auth" "github" { id = "github" } ``` + +## GitHub clone with branch name + +To GitHub clone with a specific branch like `feat/example` + +```tf +# Prompt the user for the git repo URL +data "coder_parameter" "git_repo" { + name = "git_repo" + display_name = "Git repository" + default = "https://github.com/coder/coder/tree/feat/example" +} + +# Clone the repository for branch `feat/example` +module "git_clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.12" + agent_id = coder_agent.example.id + url = data.coder_parameter.git_repo.value +} + +# Create a code-server instance for the cloned repository +module "code-server" { + source = "registry.coder.com/modules/code-server/coder" + version = "1.0.12" + agent_id = coder_agent.example.id + order = 1 + folder = "/home/${local.username}/${module.git_clone.folder_name}" +} + +# Create a Coder app for the website +resource "coder_app" "website" { + agent_id = coder_agent.example.id + order = 2 + slug = "website" + external = true + display_name = module.git_clone.folder_name + url = module.git_clone.web_url + icon = module.git_clone.git_provider != "" ? "/icon/${module.git_clone.git_provider}.svg" : "/icon/git.svg" + count = module.git_clone.web_url != "" ? 1 : 0 +} +``` + +Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `github.example.com` + +```tf +module "git-clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.12" + agent_id = coder_agent.example.id + url = "https://github.example.com/coder/coder/tree/feat/example" + git_providers = { + "https://github.example.com/" = { + provider = "github" + } + } +} +``` + +## GitLab clone with branch name + +To GitLab clone with a specific branch like `feat/example` + +```tf +module "git-clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.12" + agent_id = coder_agent.example.id + url = "https://gitlab.com/coder/coder/-/tree/feat/example" +} +``` + +Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com` + +```tf +module "git-clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.12" + agent_id = coder_agent.example.id + url = "https://gitlab.example.com/coder/coder/-/tree/feat/example" + git_providers = { + "https://gitlab.example.com/" = { + provider = "gitlab" + } + } +} +``` + +## Git clone with branch_name set + +Alternatively, you can set the `branch_name` attribute to clone a specific branch. + +For example, to clone the `feat/example` branch: + +```tf +module "git-clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.12" + agent_id = coder_agent.example.id + url = "https://github.com/coder/coder" + branch_name = "feat/example" +} +``` diff --git a/git-clone/main.test.ts b/git-clone/main.test.ts index 0c3dd54..87b0e4a 100644 --- a/git-clone/main.test.ts +++ b/git-clone/main.test.ts @@ -36,4 +36,196 @@ describe("git-clone", async () => { "Cloning fake-url to ~/fake-url...", ]); }); + + it("repo_dir should match repo name for https", async () => { + const url = "https://github.com/coder/coder.git"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/coder"); + expect(state.outputs.folder_name.value).toEqual("coder"); + expect(state.outputs.clone_url.value).toEqual(url); + expect(state.outputs.web_url.value).toEqual(url); + expect(state.outputs.branch_name.value).toEqual(""); + }); + + it("repo_dir should match repo name for https without .git", async () => { + const url = "https://github.com/coder/coder"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/coder"); + expect(state.outputs.clone_url.value).toEqual(url); + expect(state.outputs.web_url.value).toEqual(url); + expect(state.outputs.branch_name.value).toEqual(""); + }); + + it("repo_dir should match repo name for ssh", async () => { + const url = "git@github.com:coder/coder.git"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/coder"); + expect(state.outputs.git_provider.value).toEqual(""); + expect(state.outputs.clone_url.value).toEqual(url); + const https_url = "https://github.com/coder/coder.git"; + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual(""); + }); + + it("branch_name should not include query string", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch?ref_type=heads", + }); + expect(state.outputs.repo_dir.value).toEqual("~/repo-tests.log"); + expect(state.outputs.folder_name.value).toEqual("repo-tests.log"); + const https_url = "https://gitlab.com/mike.brew/repo-tests.log"; + expect(state.outputs.clone_url.value).toEqual(https_url); + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual("feat/branch"); + }); + + it("branch_name should not include fragments", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch#name", + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log"); + const https_url = "https://gitlab.com/mike.brew/repo-tests.log"; + expect(state.outputs.clone_url.value).toEqual(https_url); + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual("feat/branch"); + }); + + it("gitlab url with branch should match", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch", + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log"); + expect(state.outputs.git_provider.value).toEqual("gitlab"); + const https_url = "https://gitlab.com/mike.brew/repo-tests.log"; + expect(state.outputs.clone_url.value).toEqual(https_url); + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual("feat/branch"); + }); + + it("github url with branch should match", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url: "https://github.com/michaelbrewer/repo-tests.log/tree/feat/branch", + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log"); + expect(state.outputs.git_provider.value).toEqual("github"); + const https_url = "https://github.com/michaelbrewer/repo-tests.log"; + expect(state.outputs.clone_url.value).toEqual(https_url); + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual("feat/branch"); + }); + + it("self-host git url with branch should match", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url: "https://git.example.com/example/project/-/tree/feat/example", + git_providers: ` + { + "https://git.example.com/" = { + provider = "gitlab" + } + }`, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/project"); + expect(state.outputs.git_provider.value).toEqual("gitlab"); + const https_url = "https://git.example.com/example/project"; + expect(state.outputs.clone_url.value).toEqual(https_url); + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual("feat/example"); + }); + + it("handle unsupported git provider configuration", async () => { + const t = async () => { + await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "foo", + git_providers: ` + { + "https://git.example.com/" = { + provider = "bitbucket" + } + }`, + }); + }; + expect(t).toThrow('Allowed values for provider are "github" or "gitlab".'); + }); + + it("handle unknown git provider url", async () => { + const url = "https://git.unknown.com/coder/coder"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/coder"); + expect(state.outputs.clone_url.value).toEqual(url); + expect(state.outputs.web_url.value).toEqual(url); + expect(state.outputs.branch_name.value).toEqual(""); + }); + + it("runs with github clone with switch to feat/branch", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "https://github.com/michaelbrewer/repo-tests.log/tree/feat/branch", + }); + const output = await executeScriptInContainer(state, "alpine/git"); + expect(output.exitCode).toBe(0); + expect(output.stdout).toEqual([ + "Creating directory ~/repo-tests.log...", + "Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...", + ]); + }); + + it("runs with gitlab clone with switch to feat/branch", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch", + }); + const output = await executeScriptInContainer(state, "alpine/git"); + expect(output.exitCode).toBe(0); + expect(output.stdout).toEqual([ + "Creating directory ~/repo-tests.log...", + "Cloning https://gitlab.com/mike.brew/repo-tests.log to ~/repo-tests.log on branch feat/branch...", + ]); + }); + + it("runs with github clone with branch_name set to feat/branch", async () => { + const url = "https://github.com/michaelbrewer/repo-tests.log"; + const branch_name = "feat/branch"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url, + branch_name, + }); + expect(state.outputs.repo_dir.value).toEqual("~/repo-tests.log"); + expect(state.outputs.clone_url.value).toEqual(url); + expect(state.outputs.web_url.value).toEqual(url); + expect(state.outputs.branch_name.value).toEqual(branch_name); + + const output = await executeScriptInContainer(state, "alpine/git"); + expect(output.exitCode).toBe(0); + expect(output.stdout).toEqual([ + "Creating directory ~/repo-tests.log...", + "Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...", + ]); + }); }); diff --git a/git-clone/main.tf b/git-clone/main.tf index c1e65cf..4af5000 100644 --- a/git-clone/main.tf +++ b/git-clone/main.tf @@ -25,8 +25,50 @@ variable "agent_id" { type = string } +variable "git_providers" { + type = map(object({ + provider = string + })) + description = "A mapping of URLs to their git provider." + default = { + "https://github.com/" = { + provider = "github" + }, + "https://gitlab.com/" = { + provider = "gitlab" + }, + } + validation { + error_message = "Allowed values for provider are \"github\" or \"gitlab\"." + condition = alltrue([for provider in var.git_providers : contains(["github", "gitlab"], provider.provider)]) + } +} + +variable "branch_name" { + description = "The branch name to clone. If not provided, the default branch will be cloned." + type = string + default = "" +} + locals { - clone_path = var.base_dir != "" ? join("/", [var.base_dir, replace(basename(var.url), ".git", "")]) : join("/", ["~", replace(basename(var.url), ".git", "")]) + # Remove query parameters and fragments from the URL + url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "") + + # Find the git provider based on the URL and determine the tree path + provider_key = try(one([for key in keys(var.git_providers) : key if startswith(local.url, key)]), null) + provider = try(lookup(var.git_providers, local.provider_key).provider, "") + tree_path = local.provider == "gitlab" ? "/-/tree/" : local.provider == "github" ? "/tree/" : "" + + # Remove tree and branch name from the URL + clone_url = var.branch_name == "" && local.tree_path != "" ? replace(local.url, "/${local.tree_path}.*/", "") : local.url + # Extract the branch name from the URL + branch_name = var.branch_name == "" && local.tree_path != "" ? replace(replace(local.url, local.clone_url, ""), "/.*${local.tree_path}/", "") : var.branch_name + # Extract the folder name from the URL + folder_name = replace(basename(local.clone_url), ".git", "") + # Construct the path to clone the repository + clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name]) + # Construct the web URL + web_url = startswith(local.clone_url, "git@") ? replace(replace(local.clone_url, ":", "/"), "git@", "https://") : local.clone_url } output "repo_dir" { @@ -34,11 +76,37 @@ output "repo_dir" { description = "Full path of cloned repo directory" } +output "git_provider" { + value = local.provider + description = "The git provider of the repository" +} + +output "folder_name" { + value = local.folder_name + description = "The name of the folder that will be created" +} + +output "clone_url" { + value = local.clone_url + description = "The exact Git repository URL that will be cloned" +} + +output "web_url" { + value = local.web_url + description = "Git https repository URL (may be invalid for unsupported providers)" +} + +output "branch_name" { + value = local.branch_name + description = "Git branch name (may be empty)" +} + resource "coder_script" "git_clone" { agent_id = var.agent_id script = templatefile("${path.module}/run.sh", { - CLONE_PATH = local.clone_path - REPO_URL : var.url, + CLONE_PATH = local.clone_path, + REPO_URL : local.clone_url, + BRANCH_NAME : local.branch_name, }) display_name = "Git Clone" icon = "/icon/git.svg" diff --git a/git-clone/run.sh b/git-clone/run.sh index df647a1..bd80717 100755 --- a/git-clone/run.sh +++ b/git-clone/run.sh @@ -2,6 +2,7 @@ REPO_URL="${REPO_URL}" CLONE_PATH="${CLONE_PATH}" +BRANCH_NAME="${BRANCH_NAME}" # Expand home if it's specified! CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}" @@ -33,8 +34,13 @@ fi # Check if the directory is empty # and if it is, clone the repo, otherwise skip cloning if [ -z "$(ls -A "$CLONE_PATH")" ]; then - echo "Cloning $REPO_URL to $CLONE_PATH..." - git clone "$REPO_URL" "$CLONE_PATH" + if [ -z "$BRANCH_NAME" ]; then + echo "Cloning $REPO_URL to $CLONE_PATH..." + git clone "$REPO_URL" "$CLONE_PATH" + else + echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..." + git clone "$REPO_URL" -b "$BRANCH_NAME" "$CLONE_PATH" + fi else echo "$CLONE_PATH already exists and isn't empty, skipping clone!" exit 0 diff --git a/git-config/README.md b/git-config/README.md index 9b76658..8a0f3ad 100644 --- a/git-config/README.md +++ b/git-config/README.md @@ -14,7 +14,7 @@ Runs a script that updates git credentials in the workspace to match the user's ```tf module "git-config" { source = "registry.coder.com/modules/git-config/coder" - version = "1.0.3" + version = "1.0.12" agent_id = coder_agent.example.id } ``` @@ -28,7 +28,7 @@ TODO: Add screenshot ```tf module "git-config" { source = "registry.coder.com/modules/git-config/coder" - version = "1.0.3" + version = "1.0.12" agent_id = coder_agent.example.id allow_email_change = true } @@ -41,7 +41,7 @@ TODO: Add screenshot ```tf module "git-config" { source = "registry.coder.com/modules/git-config/coder" - version = "1.0.3" + version = "1.0.12" agent_id = coder_agent.example.id allow_username_change = false allow_email_change = false diff --git a/jetbrains-gateway/README.md b/jetbrains-gateway/README.md index 629afb2..b2c0e0f 100644 --- a/jetbrains-gateway/README.md +++ b/jetbrains-gateway/README.md @@ -14,7 +14,7 @@ This module adds a JetBrains Gateway Button to open any workspace with a single ```tf module "jetbrains_gateway" { source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.11" + version = "1.0.13" agent_id = coder_agent.example.id agent_name = "example" folder = "/home/coder/example" @@ -27,12 +27,12 @@ module "jetbrains_gateway" { ## Examples -### Add GoLand and WebStorm with the default set to GoLand +### Add GoLand and WebStorm as options with the default set to GoLand ```tf module "jetbrains_gateway" { source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.11" + version = "1.0.13" agent_id = coder_agent.example.id agent_name = "example" folder = "/home/coder/example" @@ -41,6 +41,37 @@ module "jetbrains_gateway" { } ``` +### Use the latest release version + +```tf +module "jetbrains_gateway" { + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.13" + agent_id = coder_agent.example.id + agent_name = "example" + folder = "/home/coder/example" + jetbrains_ides = ["GO", "WS"] + default = "GO" + latest = true +} +``` + +### Use the latest EAP version + +```tf +module "jetbrains_gateway" { + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.13" + agent_id = coder_agent.example.id + agent_name = "example" + folder = "/home/coder/example" + jetbrains_ides = ["GO", "WS"] + default = "GO" + latest = true + channel = "eap" +} +``` + ## Supported IDEs This module and JetBrains Gateway support the following JetBrains IDEs: diff --git a/jetbrains-gateway/main.tf b/jetbrains-gateway/main.tf index 7c57bee..c96098c 100644 --- a/jetbrains-gateway/main.tf +++ b/jetbrains-gateway/main.tf @@ -6,6 +6,10 @@ terraform { source = "coder/coder" version = ">= 0.17" } + http = { + source = "hashicorp/http" + version = ">= 3.0" + } } } @@ -46,6 +50,22 @@ variable "coder_parameter_order" { default = null } +variable "latest" { + type = bool + description = "Whether to fetch the latest version of the IDE." + default = false +} + +variable "channel" { + type = string + description = "JetBrains IDE release channel. Valid values are release and eap." + default = "release" + validation { + condition = can(regex("^(release|eap)$", var.channel)) + error_message = "The channel must be either release or eap." + } +} + variable "jetbrains_ide_versions" { type = map(object({ build_number = string @@ -120,6 +140,11 @@ variable "jetbrains_ides" { } } +data "http" "jetbrains_ide_versions" { + for_each = var.latest ? toset(var.jetbrains_ides) : toset([]) + url = "https://data.services.jetbrains.com/products/releases?code=${each.key}&latest=true&type=${var.channel}" +} + locals { jetbrains_ides = { "GO" = { @@ -128,6 +153,7 @@ locals { identifier = "GO", build_number = var.jetbrains_ide_versions["GO"].build_number, download_link = "https://download.jetbrains.com/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz" + version = var.jetbrains_ide_versions["GO"].version }, "WS" = { icon = "/icon/webstorm.svg", @@ -135,6 +161,7 @@ locals { identifier = "WS", build_number = var.jetbrains_ide_versions["WS"].build_number, download_link = "https://download.jetbrains.com/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz" + version = var.jetbrains_ide_versions["WS"].version }, "IU" = { icon = "/icon/intellij.svg", @@ -142,6 +169,7 @@ locals { identifier = "IU", build_number = var.jetbrains_ide_versions["IU"].build_number, download_link = "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz" + version = var.jetbrains_ide_versions["IU"].version }, "PY" = { icon = "/icon/pycharm.svg", @@ -149,6 +177,7 @@ locals { identifier = "PY", build_number = var.jetbrains_ide_versions["PY"].build_number, download_link = "https://download.jetbrains.com/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz" + version = var.jetbrains_ide_versions["PY"].version }, "CL" = { icon = "/icon/clion.svg", @@ -156,6 +185,7 @@ locals { identifier = "CL", build_number = var.jetbrains_ide_versions["CL"].build_number, download_link = "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz" + version = var.jetbrains_ide_versions["CL"].version }, "PS" = { icon = "/icon/phpstorm.svg", @@ -163,6 +193,7 @@ locals { identifier = "PS", build_number = var.jetbrains_ide_versions["PS"].build_number, download_link = "https://download.jetbrains.com/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz" + version = var.jetbrains_ide_versions["PS"].version }, "RM" = { icon = "/icon/rubymine.svg", @@ -170,6 +201,7 @@ locals { identifier = "RM", build_number = var.jetbrains_ide_versions["RM"].build_number, download_link = "https://download.jetbrains.com/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz" + version = var.jetbrains_ide_versions["RM"].version } "RD" = { icon = "/icon/rider.svg", @@ -177,8 +209,18 @@ locals { identifier = "RD", build_number = var.jetbrains_ide_versions["RD"].build_number, download_link = "https://download.jetbrains.com/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz" + version = var.jetbrains_ide_versions["RD"].version } } + + icon = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].icon + json_data = var.latest ? jsondecode(data.http.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].response_body) : {} + key = var.latest ? keys(local.json_data)[0] : "" + display_name = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name + identifier = data.coder_parameter.jetbrains_ide.value + download_link = var.latest ? local.json_data[local.key][0].downloads.linux.link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link + build_number = var.latest ? local.json_data[local.key][0].build : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number + version = var.latest ? local.json_data[local.key][0].version : var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version } data "coder_parameter" "jetbrains_ide" { @@ -193,9 +235,9 @@ data "coder_parameter" "jetbrains_ide" { dynamic "option" { for_each = var.jetbrains_ides content { - icon = lookup(local.jetbrains_ides, option.value).icon - name = lookup(local.jetbrains_ides, option.value).name - value = lookup(local.jetbrains_ides, option.value).identifier + icon = local.jetbrains_ides[option.value].icon + name = local.jetbrains_ides[option.value].name + value = option.value } } } @@ -205,8 +247,8 @@ data "coder_workspace" "me" {} resource "coder_app" "gateway" { agent_id = var.agent_id slug = "gateway" - display_name = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).name, "JetBrains IDE") - icon = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).icon, "/icon/gateway.svg") + display_name = local.display_name + icon = local.icon external = true order = var.order url = join("", [ @@ -221,36 +263,36 @@ resource "coder_app" "gateway" { "&token=", "$SESSION_TOKEN", "&ide_product_code=", - local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].identifier, + data.coder_parameter.jetbrains_ide.value, "&ide_build_number=", - local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number, + local.build_number, "&ide_download_link=", - local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link + local.download_link, ]) } output "identifier" { - value = data.coder_parameter.jetbrains_ide.value + value = local.identifier } -output "name" { - value = coder_app.gateway.display_name +output "display_name" { + value = local.display_name } output "icon" { - value = coder_app.gateway.icon + value = local.icon } output "download_link" { - value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).download_link + value = local.download_link } output "build_number" { - value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).build_number + value = local.build_number } output "version" { - value = var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version + value = local.version } output "url" { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8039c1c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,263 @@ +{ + "name": "modules", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "modules", + "devDependencies": { + "bun-types": "^1.0.18", + "gray-matter": "^4.0.3", + "marked": "^12.0.0", + "prettier": "^3.2.5", + "prettier-plugin-sh": "^0.13.1", + "prettier-plugin-terraform-formatter": "^1.2.1" + }, + "peerDependencies": { + "typescript": "^5.3.3" + } + }, + "node_modules/@types/node": { + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/bun-types": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.4.tgz", + "integrity": "sha512-E1kk0FNpxpkSSlCVXEa4HfyhSUEpKtCFrybPVyz1A4TEnBGy5bqqtSYkyjKTfKScdyZTBeFrTxJLiKGOIRWgwg==", + "dev": true, + "dependencies": { + "@types/node": "~20.11.3", + "@types/ws": "~8.5.10" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mvdan-sh": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz", + "integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==", + "dev": true + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-sh": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.13.1.tgz", + "integrity": "sha512-ytMcl1qK4s4BOFGvsc9b0+k9dYECal7U29bL/ke08FEUsF/JLN0j6Peo0wUkFDG4y2UHLMhvpyd6Sd3zDXe/eg==", + "dev": true, + "dependencies": { + "mvdan-sh": "^0.10.1", + "sh-syntax": "^0.4.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, + "node_modules/prettier-plugin-terraform-formatter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-terraform-formatter/-/prettier-plugin-terraform-formatter-1.2.1.tgz", + "integrity": "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA==", + "dev": true, + "peerDependencies": { + "prettier": ">= 1.16.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sh-syntax": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz", + "integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/package.json b/package.json index 5a73d51..f3136b1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "bun-types": "^1.0.18", "gray-matter": "^4.0.3", "marked": "^12.0.0", + "prettier": "^3.2.5", "prettier-plugin-sh": "^0.13.1", "prettier-plugin-terraform-formatter": "^1.2.1" }, @@ -23,4 +24,4 @@ "prettier-plugin-terraform-formatter" ] } -} \ No newline at end of file +}