Compare commits

...

10 Commits

Author SHA1 Message Date
Atif Ali f98d3926c5 Merge branch 'main' into atif/vscode-core-extenions 2025-10-30 10:51:49 +05:00
DevelopmentCats 301ef88735 refactor(vscode-desktop-core): fix package.json detection 2025-10-17 15:35:40 -05:00
DevelopmentCats 7bbf5cf1c4 fix(vscode-desktop-core): enhance logging for extension installation process with detailed output for troubleshooting 2025-10-17 15:17:30 -05:00
DevelopmentCats c5a76ab005 fix(vscode-desktop-core): update extension extraction and download methods, and add module dir with logging paths for troubleshooting 2025-10-17 14:58:38 -05:00
DevCats e336fa8978 Merge branch 'main' into atif/vscode-core-extenions 2025-10-17 10:22:59 -05:00
DevCats 814224ffa3 Merge branch 'main' into atif/vscode-core-extenions 2025-10-16 16:41:30 -05:00
DevCats cb8c89b454 Merge branch 'main' into atif/vscode-core-extenions 2025-10-15 14:16:11 -05:00
Atif Ali 3c6eb16a5e Merge branch 'main' into atif/vscode-core-extenions 2025-10-15 18:00:05 +05:00
Atif Ali 349774eb33 Merge branch 'main' into atif/vscode-core-extenions 2025-10-06 19:05:15 +05:00
Muhammad Atif Ali 2687987742 Add extension install logic to vscode-desktop-core
- Add logic for handling VS Code and non-MS IDEs in scripts.
- Introduce Terraform variables for extension details.
- Implement validation for protocol selection.
- Include tests to validate extension install paths and mutual exclusions.
2025-09-24 14:59:40 +05:00
4 changed files with 707 additions and 10 deletions
@@ -1,9 +1,12 @@
import { describe, expect, it } from "bun:test";
import { describe, expect, it, beforeAll, afterAll } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
import { mkdtempSync, rmSync, existsSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
// hardcoded coder_app name in main.tf
const appName = "vscode-desktop";
@@ -39,7 +42,6 @@ describe("vscode-desktop-core", async () => {
it("adds folder", async () => {
const state = await runTerraformApply(import.meta.dir, {
folder: "/foo/bar",
...defaultVariables,
});
@@ -52,7 +54,6 @@ describe("vscode-desktop-core", async () => {
const state = await runTerraformApply(import.meta.dir, {
folder: "/foo/bar",
open_recent: "true",
...defaultVariables,
});
expect(state.outputs.ide_uri.value).toBe(
@@ -64,7 +65,6 @@ describe("vscode-desktop-core", async () => {
const state = await runTerraformApply(import.meta.dir, {
folder: "/foo/bar",
openRecent: "false",
...defaultVariables,
});
expect(state.outputs.ide_uri.value).toBe(
@@ -75,7 +75,6 @@ describe("vscode-desktop-core", async () => {
it("adds open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
open_recent: "true",
...defaultVariables,
});
expect(state.outputs.ide_uri.value).toBe(
@@ -98,3 +97,202 @@ describe("vscode-desktop-core", async () => {
expect(coder_app?.instances[0].attributes.order).toBe(22);
});
});
describe("vscode-desktop-core extension script logic", async () => {
await runTerraformInit(import.meta.dir);
let tempDir: string;
beforeAll(() => {
tempDir = mkdtempSync(join(tmpdir(), "vscode-extensions-test-"));
});
afterAll(() => {
if (tempDir && existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true });
}
});
const supportedIdes = [
{
protocol: "vscode",
name: "VS Code",
expectedUrls: [
"marketplace.visualstudio.com/_apis/public/gallery/vscode/",
],
marketplace: "Microsoft",
},
{
protocol: "vscode-insiders",
name: "VS Code Insiders",
expectedUrls: [
"marketplace.visualstudio.com/_apis/public/gallery/vscode/",
],
marketplace: "Microsoft",
},
{
protocol: "vscodium",
name: "VSCodium",
expectedUrls: ["open-vsx.org/api/"],
marketplace: "Open VSX",
},
{
protocol: "cursor",
name: "Cursor",
expectedUrls: ["open-vsx.org/api/"],
marketplace: "Open VSX",
},
{
protocol: "windsurf",
name: "WindSurf",
expectedUrls: ["open-vsx.org/api/"],
marketplace: "Open VSX",
},
{
protocol: "kiro",
name: "Kiro",
expectedUrls: ["open-vsx.org/api/"],
marketplace: "Open VSX",
},
];
// Test extension script generation and IDE-specific marketplace logic
for (const ide of supportedIdes) {
it(`should use correct marketplace for ${ide.name} (${ide.marketplace})`, async () => {
const extensionsDir = join(tempDir, ide.protocol, "extensions");
const variables = {
...defaultVariables,
protocol: ide.protocol,
coder_app_display_name: ide.name,
extensions: '["ms-vscode.hexeditor"]',
extensions_dir: extensionsDir,
};
const state = await runTerraformApply(import.meta.dir, variables);
// Verify the script was created
const extensionScript = state.resources.find(
(res) =>
res.type === "coder_script" && res.name === "extensions-installer",
);
expect(extensionScript).not.toBeNull();
const scriptContent = extensionScript?.instances[0].attributes.script;
// Verify IDE type is correctly set
expect(scriptContent).toContain(`IDE_TYPE="${ide.protocol}"`);
// Verify extensions directory is set correctly
expect(scriptContent).toContain(`EXTENSIONS_DIR="${extensionsDir}"`);
// Verify extension ID is present
expect(scriptContent).toContain("ms-vscode.hexeditor");
// Verify the case statement includes the IDE protocol (Terraform substitutes the variable)
expect(scriptContent).toContain(`case "${ide.protocol}" in`);
// Verify that the correct case branch exists for the IDE
if (ide.marketplace === "Microsoft") {
expect(scriptContent).toContain(`"vscode" | "vscode-insiders"`);
} else {
expect(scriptContent).toContain(
`"vscodium" | "cursor" | "windsurf" | "kiro"`,
);
}
// Verify the correct marketplace URL is present
for (const expectedUrl of ide.expectedUrls) {
expect(scriptContent).toContain(expectedUrl);
}
});
}
// Test extension installation from URLs (airgapped scenario)
it("should generate script for extensions from URLs with proper variable handling", async () => {
const extensionsDir = join(tempDir, "airgapped", "extensions");
const variables = {
...defaultVariables,
extensions_urls:
'["https://marketplace.visualstudio.com/_apis/public/gallery/vscode/ms-vscode/hexeditor/latest"]',
extensions_dir: extensionsDir,
};
const state = await runTerraformApply(import.meta.dir, variables);
const extensionScript = state.resources.find(
(res) =>
res.type === "coder_script" && res.name === "extensions-installer",
);
expect(extensionScript).not.toBeNull();
const scriptContent = extensionScript?.instances[0].attributes.script;
// Verify URLs variable is populated
expect(scriptContent).toContain("EXTENSIONS_URLS=");
expect(scriptContent).toContain("hexeditor");
// Verify extensions variable is empty when using URLs
expect(scriptContent).toContain('EXTENSIONS=""');
// Verify the script calls the URL installation function
expect(scriptContent).toContain("install_extensions_from_urls");
});
// Test script logic for both extension IDs and URLs handling
it("should handle empty extensions gracefully", async () => {
const variables = {
...defaultVariables,
extensions: "[]",
extensions_urls: "[]",
};
const state = await runTerraformApply(import.meta.dir, variables);
// Script should not exist when no extensions are provided
const extensionScript = state.resources.find(
(res) =>
res.type === "coder_script" && res.name === "extensions-installer",
);
expect(extensionScript).toBeUndefined();
});
// Test script template variable substitution
it("should properly substitute template variables in script", async () => {
const customDir = join(tempDir, "custom-template-test");
const testExtensions = ["ms-python.python", "ms-vscode.cpptools"];
const variables = {
...defaultVariables,
protocol: "cursor",
extensions: JSON.stringify(testExtensions),
extensions_dir: customDir,
};
const state = await runTerraformApply(import.meta.dir, variables);
const extensionScript = state.resources.find(
(res) =>
res.type === "coder_script" && res.name === "extensions-installer",
)?.instances[0].attributes.script;
// Verify all template variables are properly substituted
expect(extensionScript).toContain(
`EXTENSIONS="${testExtensions.join(",")}"`,
);
expect(extensionScript).toContain(`EXTENSIONS_URLS=""`);
expect(extensionScript).toContain(`EXTENSIONS_DIR="${customDir}"`);
expect(extensionScript).toContain(`IDE_TYPE="cursor"`);
// Verify Terraform template variables are properly substituted (no double braces)
expect(extensionScript).not.toContain("$${");
// Verify script contains proper bash functions
expect(extensionScript).toContain("generate_extension_url()");
expect(extensionScript).toContain("install_extensions_from_ids");
expect(extensionScript).toContain("install_extensions_from_urls");
});
});
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 2.11"
}
}
}
@@ -14,6 +14,30 @@ variable "agent_id" {
description = "The ID of a Coder agent."
}
variable "extensions" {
type = list(string)
description = <<-EOF
The list of extensions to install in the IDE.
Example: ["ms-python.python", "ms-vscode.cpptools"]
EOF
default = []
}
variable "extensions_urls" {
type = list(string)
description = <<-EOF
The list of extension URLs to install in the IDE.
Example: ["https://marketplace.visualstudio.com/items?itemName=ms-python.python", "https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools"]
EOF
default = []
}
variable "extensions_dir" {
type = string
description = "The directory where extensions will be installed."
default = ""
}
variable "folder" {
type = string
description = "The folder to open in the IDE."
@@ -29,6 +53,10 @@ variable "open_recent" {
variable "protocol" {
type = string
description = "The URI protocol for the IDE."
validation {
condition = contains(["vscode", "vscode-insiders", "vscodium", "cursor", "windsurf", "kiro"], var.protocol)
error_message = "Protocol must be one of: vscode, vscode-insiders, vscodium, cursor, windsurf, or kiro."
}
}
variable "coder_app_icon" {
@@ -58,9 +86,50 @@ variable "coder_app_group" {
default = null
}
variable "coder_app_tooltip" {
type = string
description = "An optional tooltip to display on the IDE button."
default = null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
default_extensions_dirs = {
vscode = "~/.vscode-server/extensions"
vscode-insiders = "~/.vscode-server-insiders/extensions"
vscodium = "~/.vscode-server-oss/extensions"
cursor = "~/.cursor-server/extensions"
windsurf = "~/.windsurf-server/extensions"
kiro = "~/.kiro-server/extensions"
}
# Extensions directory
final_extensions_dir = var.extensions_dir != "" ? var.extensions_dir : local.default_extensions_dirs[var.protocol]
}
resource "coder_script" "extensions-installer" {
count = length(var.extensions) > 0 || length(var.extensions_urls) > 0 ? 1 : 0
agent_id = var.agent_id
display_name = "${var.coder_app_display_name} Extensions"
icon = var.coder_app_icon
script = templatefile("${path.module}/run.sh", {
EXTENSIONS = join(",", var.extensions)
EXTENSIONS_URLS = join(",", var.extensions_urls)
EXTENSIONS_DIR = local.final_extensions_dir
IDE_TYPE = var.protocol
})
run_on_start = true
lifecycle {
precondition {
condition = !(length(var.extensions) > 0 && length(var.extensions_urls) > 0)
error_message = "Cannot specify both 'extensions' and 'extensions_urls'. Use 'extensions' for normal operation or 'extensions_urls' for airgapped environments."
}
}
}
resource "coder_app" "vscode-desktop" {
agent_id = var.agent_id
external = true
@@ -68,9 +137,9 @@ resource "coder_app" "vscode-desktop" {
icon = var.coder_app_icon
slug = var.coder_app_slug
display_name = var.coder_app_display_name
order = var.coder_app_order
group = var.coder_app_group
order = var.coder_app_order
group = var.coder_app_group
tooltip = var.coder_app_tooltip
# While the call to "join" is not strictly necessary, it makes the URL more readable.
url = join("", [
@@ -89,4 +158,4 @@ resource "coder_app" "vscode-desktop" {
output "ide_uri" {
value = coder_app.vscode-desktop.url
description = "IDE URI."
}
}
@@ -0,0 +1,330 @@
#!/usr/bin/env bash
# shellcheck disable=SC2269 # Terraform template variables
# shellcheck disable=SC2034 # Color variables used in Terraform templates
# shellcheck disable=SC2059 # printf format strings with Terraform variables
set -euo pipefail
EXTENSIONS="${EXTENSIONS}"
EXTENSIONS_URLS="${EXTENSIONS_URLS}"
EXTENSIONS_DIR="${EXTENSIONS_DIR}"
IDE_TYPE="${IDE_TYPE}"
BOLD='\033[0;1m'
CODE='\033[36;40;1m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
RESET='\033[0m'
is_extension_installed() {
local target_dir="$1"
local extension_id="$2"
local extension_dir="$target_dir/$extension_id"
local package_json=""
if [ -f "$extension_dir/package.json" ]; then
package_json="$extension_dir/package.json"
elif [ -f "$extension_dir/extension/package.json" ]; then
package_json="$extension_dir/extension/package.json"
fi
if [ -d "$extension_dir" ] && [ -n "$package_json" ]; then
if grep -q '"name"' "$package_json" 2> /dev/null; then
if grep -q '"publisher"' "$package_json" 2> /dev/null; then
return 0
fi
fi
fi
return 1
}
generate_extension_url() {
local extension_id="$1"
if [[ -z "$extension_id" ]]; then
return 1
fi
local publisher
publisher=$(echo "$extension_id" | cut -d'.' -f1)
local name
name=$(echo "$extension_id" | cut -d'.' -f2-)
if [[ -z "$publisher" ]] || [[ -z "$name" ]]; then
printf "$${RED}❌ Invalid extension ID format: $extension_id$${RESET}\n" >&2
return 1
fi
case "${IDE_TYPE}" in
"vscode" | "vscode-insiders")
printf "https://marketplace.visualstudio.com/_apis/public/gallery/vscode/%s/%s/latest" "$publisher" "$name"
;;
"vscodium" | "cursor" | "windsurf" | "kiro")
printf "https://open-vsx.org/api/%s/%s/latest" "$publisher" "$name"
;;
*)
printf "https://open-vsx.org/api/%s/%s/latest" "$publisher" "$name"
;;
esac
}
download_and_install_extension() {
local target_dir="$1"
local extension_id="$2"
local metadata_url="$3"
local temp_dir="$4"
local log_file="$5"
if is_extension_installed "$target_dir" "$extension_id"; then
printf "$${GREEN}✓ Extension $${CODE}$extension_id$${RESET}$${GREEN} already installed$${RESET}\n"
return 0
fi
printf "$${BOLD}📦 Installing extension $${CODE}$extension_id$${RESET}...\n"
echo "$(date): Starting installation of $extension_id" >> "$log_file"
local extension_temp_dir
extension_temp_dir="$temp_dir/$extension_id-$(date +%s)"
local download_file="$temp_dir/$extension_id.vsix"
echo "$(date): Fetching metadata from $metadata_url" >> "$log_file"
local metadata_response
if metadata_response=$(timeout 30 curl -fsSL "$metadata_url" 2>&1); then
local download_url
if [[ "${IDE_TYPE}" == "vscode" || "${IDE_TYPE}" == "vscode-insiders" ]]; then
download_url=$(echo "$metadata_response" | jq -r '.versions[0].files[] | select(.assetType == "Microsoft.VisualStudio.Services.VSIXPackage") | .source' 2> /dev/null)
else
download_url=$(echo "$metadata_response" | jq -r '.files.download // .downloads.universal // empty' 2> /dev/null)
fi
if [[ -n "$download_url" && "$download_url" != "null" ]]; then
echo "$(date): Extracted download URL: $download_url" >> "$log_file"
echo "$(date): Downloading extension to $download_file" >> "$log_file"
if timeout 30 curl -fsSL "$download_url" -o "$download_file" 2>&1; then
echo "$(date): File size: $(stat -c%s "$download_file") bytes" >> "$log_file"
echo "$(date): Validating ZIP file..." >> "$log_file"
if unzip -t "$download_file" > /dev/null 2>&1; then
mkdir -p "$target_dir"
local extract_dir="$target_dir/$extension_id"
if [ -d "$extract_dir" ]; then
rm -rf "$extract_dir"
fi
mkdir -p "$extract_dir"
echo "$(date): Extracting to $extract_dir" >> "$log_file"
if unzip -q "$download_file" -d "$extract_dir" 2> /dev/null; then
if [ -f "$extract_dir/package.json" ] || [ -f "$extract_dir/extension/package.json" ]; then
printf "$${GREEN}✅ Successfully installed $${CODE}$extension_id$${RESET}\n"
echo "$(date): Successfully installed $extension_id" >> "$log_file"
rm -rf "$extension_temp_dir"
return 0
else
printf "$${RED}❌ Invalid extension package$${RESET}\n"
echo "$(date): Invalid extension package for $extension_id - package.json not found" >> "$log_file"
echo "$(date): Directory contents: $(ls -la "$extract_dir")" >> "$log_file"
rm -rf "$extract_dir"
rm -rf "$extension_temp_dir"
return 1
fi
else
printf "$${RED}❌ Failed to extract extension$${RESET}\n"
echo "$(date): Failed to extract $extension_id" >> "$log_file"
rm -rf "$extract_dir"
rm -rf "$extension_temp_dir"
return 1
fi
else
printf "$${RED}❌ Invalid file format$${RESET}\n"
{
echo "$(date): ZIP validation failed for $extension_id"
echo "$(date): File size: $(stat -c%s "$download_file") bytes"
echo "$(date): First 100 bytes: $(head -c 100 "$download_file" | hexdump -C | head -3)"
} >> "$log_file"
rm -rf "$extension_temp_dir"
return 1
fi
else
printf "$${RED}❌ Download failed$${RESET}\n"
echo "$(date): Download failed for $extension_id from $download_url" >> "$log_file"
rm -rf "$extension_temp_dir"
return 1
fi
else
printf "$${RED}❌ Could not extract download URL from metadata$${RESET}\n"
echo "$(date): Could not extract download URL for $extension_id" >> "$log_file"
rm -rf "$extension_temp_dir"
return 1
fi
else
printf "$${RED}❌ Failed to fetch extension metadata$${RESET}\n"
echo "$(date): Failed to fetch metadata for $extension_id from $metadata_url" >> "$log_file"
rm -rf "$extension_temp_dir"
return 1
fi
}
install_extension_from_url() {
local url="$1"
local target_dir="$2"
local temp_dir="$3"
local log_file="$4"
local extension_name
extension_name=$(basename "$url" | sed 's/\.vsix$$//')
local extension_id="$extension_name"
printf "$${BOLD}📦 Installing extension from URL: $${CODE}$extension_name$${RESET}...\n"
echo "$(date): Starting installation of $extension_id from URL: $url" >> "$log_file"
if [[ -d "$target_dir/$extension_id" ]] && [[ -f "$target_dir/$extension_id/package.json" ]]; then
printf "$${GREEN}✓ Extension $${CODE}$extension_id$${RESET}$${GREEN} already installed$${RESET}\n"
return 0
fi
local extension_temp_dir
extension_temp_dir="$temp_dir/$extension_id-$(date +%s)"
local download_file="$temp_dir/$extension_id.vsix"
echo "$(date): Downloading extension to $download_file" >> "$log_file"
if timeout 30 curl -fsSL "$url" -o "$download_file" 2>&1; then
echo "$(date): File size: $(stat -c%s "$download_file") bytes" >> "$log_file"
mkdir -p "$target_dir"
local extract_dir="$target_dir/$extension_id"
if [ -d "$extract_dir" ]; then
rm -rf "$extract_dir"
fi
mkdir -p "$extract_dir"
echo "$(date): Extracting to $extract_dir" >> "$log_file"
if unzip -q "$download_file" -d "$extract_dir" 2> /dev/null; then
if [ -f "$extract_dir/package.json" ] || [ -f "$extract_dir/extension/package.json" ]; then
printf "$${GREEN}✅ Successfully installed $${CODE}$extension_id$${RESET}\n"
echo "$(date): Successfully installed $extension_id from URL" >> "$log_file"
rm -rf "$extension_temp_dir"
return 0
else
printf "$${RED}❌ Invalid extension package$${RESET}\n"
echo "$(date): Invalid extension package for $extension_id from URL - package.json not found" >> "$log_file"
echo "$(date): Directory contents: $(ls -la "$extract_dir")" >> "$log_file"
rm -rf "$extract_dir"
rm -rf "$extension_temp_dir"
return 1
fi
else
printf "$${RED}❌ Failed to extract extension$${RESET}\n"
echo "$(date): Failed to extract $extension_id from URL" >> "$log_file"
rm -rf "$extract_dir"
rm -rf "$extension_temp_dir"
return 1
fi
else
printf "$${RED}❌ Failed to download extension from URL$${RESET}\n"
echo "$(date): Failed to download $extension_id from URL: $url" >> "$log_file"
rm -rf "$extension_temp_dir"
return 1
fi
}
install_extensions_from_urls() {
local urls="$1"
local target_dir="$2"
local temp_dir="$3"
local log_file="$4"
if [[ -z "$urls" ]]; then
return 0
fi
printf "$${BOLD}🔗 Installing extensions from URLs...$${RESET}\n"
echo "$urls" | tr ',' '\n' | while read -r url; do
url=$(echo "$url" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$url" ]; then
install_extension_from_url "$url" "$target_dir" "$temp_dir" "$log_file"
fi
done
}
install_extensions_from_ids() {
local extensions="$1"
local target_dir="$2"
local temp_dir="$3"
local log_file="$4"
if [[ -z "$extensions" ]]; then
return 0
fi
printf "$${BOLD}🧩 Installing extensions from extension IDs...$${RESET}\n"
echo "$extensions" | tr ',' '\n' | while read -r extension_id; do
extension_id=$(echo "$extension_id" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$extension_id" ]; then
local metadata_url
metadata_url=$(generate_extension_url "$extension_id")
if [ -n "$metadata_url" ]; then
download_and_install_extension "$target_dir" "$extension_id" "$metadata_url" "$temp_dir" "$log_file"
else
printf "$${RED}❌ Invalid extension ID: $extension_id$${RESET}\n"
echo "$(date): Invalid extension ID: $extension_id" >> "$log_file"
fi
fi
done
}
main() {
printf "$${BOLD}🚀 Starting extension installation for $${CODE}${IDE_TYPE}$${RESET} IDE...\n"
for cmd in curl unzip timeout; do
if ! command -v "$cmd" > /dev/null 2>&1; then
printf "$${RED}❌ Missing required command: $cmd$${RESET}\n"
return 1
fi
done
local module_dir="$HOME/.vscode-desktop-core"
local temp_dir="$module_dir/tmp"
local logs_dir="$module_dir/logs"
mkdir -p "$temp_dir" "$logs_dir"
local log_file
log_file="$logs_dir/extension-installation-$(date +%Y%m%d-%H%M%S).log"
printf "$${BOLD}📝 Logging to: $${CODE}$log_file$${RESET}\n"
local extensions_dir="${EXTENSIONS_DIR}"
if [ "$${extensions_dir#\~}" != "$extensions_dir" ]; then
extensions_dir="$HOME/$${extensions_dir#\~/}"
fi
printf "$${BOLD}📁 Using extensions directory: $${CODE}$extensions_dir$${RESET}\n"
mkdir -p "$extensions_dir"
if [[ ! -w "$extensions_dir" ]]; then
printf "$${RED}❌ Extensions directory is not writable: $extensions_dir$${RESET}\n"
return 1
fi
if [ -n "${EXTENSIONS_URLS}" ]; then
install_extensions_from_urls "${EXTENSIONS_URLS}" "$extensions_dir" "$temp_dir" "$log_file"
fi
if [[ -n "${EXTENSIONS}" ]]; then
install_extensions_from_ids "${EXTENSIONS}" "$extensions_dir" "$temp_dir" "$log_file"
fi
printf "$${BOLD}$${GREEN}✨ Extension installation completed for $${CODE}${IDE_TYPE}$${RESET}$${BOLD}$${GREEN}!$${RESET}\n"
printf "$${BOLD}📁 Extensions installed to: $${CODE}$extensions_dir$${RESET}\n"
printf "$${BOLD}📝 Log file: $${CODE}$log_file$${RESET}\n"
}
if [[ -n "${EXTENSIONS}" ]] || [[ -n "${EXTENSIONS_URLS}" ]]; then
main
else
printf "$${BOLD}️ No extensions to install for $${CODE}${IDE_TYPE}$${RESET}\n"
fi
@@ -0,0 +1,100 @@
run "required_vars" {
command = plan
variables {
agent_id = "foo"
coder_app_icon = "/icon/code.svg"
coder_app_slug = "vscode"
coder_app_display_name = "VS Code Desktop"
protocol = "vscode"
}
}
run "default_extensions_dir_vscode" {
command = plan
variables {
agent_id = "foo"
coder_app_icon = "/icon/code.svg"
coder_app_slug = "vscode"
coder_app_display_name = "VS Code Desktop"
protocol = "vscode"
extensions = ["ms-python.python"]
}
assert {
condition = local.final_extensions_dir == "~/.vscode-server/extensions"
error_message = "Default extensions directory for vscode should be ~/.vscode-server/extensions"
}
}
run "default_extensions_dir_vscodium" {
command = plan
variables {
agent_id = "foo"
coder_app_icon = "/icon/code.svg"
coder_app_slug = "vscodium"
coder_app_display_name = "VSCodium"
protocol = "vscodium"
extensions = ["ms-python.python"]
}
assert {
condition = local.final_extensions_dir == "~/.vscode-server-oss/extensions"
error_message = "Default extensions directory for vscodium should be ~/.vscode-server-oss/extensions"
}
}
run "custom_extensions_dir_override" {
command = plan
variables {
agent_id = "foo"
coder_app_icon = "/icon/code.svg"
coder_app_slug = "vscode"
coder_app_display_name = "VS Code Desktop"
protocol = "vscode"
extensions_dir = "/custom/extensions/path"
extensions = ["ms-python.python"]
}
assert {
condition = local.final_extensions_dir == "/custom/extensions/path"
error_message = "Custom extensions directory should override default"
}
}
run "invalid_protocol_validation" {
command = plan
variables {
agent_id = "foo"
coder_app_icon = "/icon/code.svg"
coder_app_slug = "invalid"
coder_app_display_name = "Invalid IDE"
protocol = "invalid"
}
expect_failures = [
var.protocol
]
}
run "mutual_exclusion_validation" {
command = plan
variables {
agent_id = "foo"
coder_app_icon = "/icon/code.svg"
coder_app_slug = "vscode"
coder_app_display_name = "VS Code Desktop"
protocol = "vscode"
extensions = ["ms-python.python"]
extensions_urls = ["https://marketplace.visualstudio.com/test.vsix"]
}
expect_failures = [
resource.coder_script.extensions-installer
]
}