Compare commits

..

15 Commits

Author SHA1 Message Date
DevelopmentCats c5f6a00851 chore: bun fmt 2026-02-24 15:35:12 -06:00
DevelopmentCats 05e6324e41 fix: refactor extension installation in VS Code Web to use VSIX downloads 2026-02-24 15:19:49 -06:00
DevelopmentCats fd6f980610 fix: enhance extension installation process in VS Code Web by utilizing remote CLI 2026-02-24 14:56:13 -06:00
DevelopmentCats 3447d31392 fix: enhance VS Code Web server readiness check to use log output 2026-02-24 14:32:52 -06:00
DevelopmentCats 618a9b8b5d fix: improve VS Code Web server readiness check before installing extensions
- Added a function to wait for the VS Code Web server to be ready before proceeding with the installation of extensions.
- Replaced sleep commands with a conditional wait to ensure extensions are only installed after the server is confirmed to be running.
2026-02-24 14:26:22 -06:00
DevelopmentCats 847c9491af fix: adjust VS Code Web CLI execution order to ensure extensions are installed after server starts
- Added a sleep command to allow the VS Code Web server to start before installing extensions.
- Modified the script to run the VS Code Web CLI first, followed by the installation of extensions.
2026-02-24 14:02:29 -06:00
DevCats 10142cbe1c Merge branch 'main' into vscode-web-cli 2026-02-24 13:26:43 -06:00
DevelopmentCats 8ec817e33c refactor: update VS Code Web settings handling to merge with existing settings
- Changed test description to reflect merging behavior of settings.
- Updated Terraform variable description to clarify merging of settings.
- Implemented a new function in the run script to merge settings using jq or Python3.
- Adjusted the settings file creation logic to merge new settings with existing ones.
- Updated README to reflect changes in settings configuration and merging requirements.
2026-02-24 13:26:19 -06:00
Atif Ali 08bd84c529 Merge branch 'main' into vscode-web-cli 2026-02-10 10:25:08 +05:00
blink-so[bot] c493bbd490 fix: resolve SC2155 shellcheck warning in vscode-web run.sh
Separate declaration and assignment for local variable to avoid masking return values.
2026-01-12 15:47:22 +00:00
Atif Ali b55d546f03 Merge branch 'main' into vscode-web-cli 2026-01-09 17:59:39 +05:00
Atif Ali f1d5947245 Merge branch 'main' into vscode-web-cli 2026-01-08 00:23:11 +05:00
Muhammad Atif Ali e1eda2ce65 bunfmt 2026-01-05 12:20:27 +05:00
Atif Ali a1eed799aa Merge branch 'main' into vscode-web-cli 2026-01-05 12:18:55 +05:00
Muhammad Atif Ali b52c0f9f63 refactor(vscode-web): migrate to VS Code CLI with code serve-web
- Replace code-server with official VS Code CLI
- Download CLI from code.visualstudio.com using cli-alpine-* URLs
- Add release_channel variable (stable/insiders)
- Add commit_id variable to pin specific VS Code versions
- Support offline mode with fallback to code-server or cached vscode-server
- Add comprehensive bun tests for settings, extensions, and CLI arguments
- Add Terraform tests for variable validation
2025-12-31 16:28:08 +05:00
5 changed files with 1382 additions and 233 deletions
+46 -26
View File
@@ -8,13 +8,13 @@ tags: [ide, vscode, web]
# VS Code Web
Automatically install [Visual Studio Code Server](https://code.visualstudio.com/docs/remote/vscode-server) in a workspace and create an app to access it via the dashboard.
Automatically install the [VS Code CLI](https://code.visualstudio.com/docs/editor/command-line) and run `code serve-web` in a workspace to access VS Code via the browser.
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.3"
version = "2.0.0"
agent_id = coder_agent.example.id
accept_license = true
}
@@ -30,7 +30,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.3"
version = "2.0.0"
agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
@@ -44,22 +44,22 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.3"
version = "2.0.0"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
}
```
### Pre-configure Settings
### Pre-configure Machine Settings
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file:
Configure VS Code's [Machine settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file). These settings are merged with any existing machine settings on startup:
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.3"
version = "2.0.0"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -69,20 +69,7 @@ module "vscode-web" {
}
```
### Pin a specific VS Code Web version
By default, this module installs the latest. To pin a specific version, retrieve the commit ID from the [VS Code Update API](https://update.code.visualstudio.com/api/commits/stable/server-linux-x64-web) and verify its corresponding release on the [VS Code GitHub Releases](https://github.com/microsoft/vscode/releases).
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.3"
agent_id = coder_agent.example.id
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
accept_license = true
}
```
> **Note:** Merging settings requires `jq` or `python3`. If neither is available, existing machine settings will be preserved. User settings configured through the VS Code UI are stored in browser local storage and will not persist across different browsers or devices.
### Open an existing workspace on startup
@@ -91,10 +78,43 @@ Note: Either `workspace` or `folder` can be used, but not both simultaneously. T
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.4.3"
agent_id = coder_agent.example.id
workspace = "/home/coder/coder.code-workspace"
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "2.0.0"
agent_id = coder_agent.example.id
workspace = "/home/coder/coder.code-workspace"
accept_license = true
}
```
### Use VS Code Insiders
Use the VS Code Insiders release channel to get the latest features and bug fixes:
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "2.0.0"
agent_id = coder_agent.example.id
release_channel = "insiders"
accept_license = true
}
```
### Pin a specific VS Code version
Use the `commit_id` variable to pin a specific VS Code Server version by its commit SHA:
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "2.0.0"
agent_id = coder_agent.example.id
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
accept_license = true
}
```
You can find the commit SHA for a specific VS Code version on the [VS Code releases page](https://code.visualstudio.com/updates) or by checking the "About" dialog in VS Code.
+778 -82
View File
@@ -1,97 +1,793 @@
import { describe, expect, it } from "bun:test";
import {
execContainer,
findResourceInstance,
readFileContainer,
removeContainer,
runContainer,
describe,
expect,
it,
beforeAll,
afterEach,
setDefaultTimeout,
} from "bun:test";
import {
runTerraformApply,
runTerraformInit,
runContainer,
execContainer,
removeContainer,
findResourceInstance,
} from "~test";
describe("vscode-web", async () => {
await runTerraformInit(import.meta.dir);
// Set timeout to 5 minutes for tests that download VS Code CLI
setDefaultTimeout(5 * 60 * 1000);
it("accept_license should be set to true", () => {
const t = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: "false",
});
};
expect(t).toThrow("Invalid value for variable");
});
let cleanupContainers: string[] = [];
it("use_cached and offline can not be used together", () => {
const t = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: "true",
use_cached: "true",
offline: "true",
});
};
expect(t).toThrow("Offline and Use Cached can not be used together");
});
it("offline and extensions can not be used together", () => {
const t = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: "true",
offline: "true",
extensions: '["1", "2"]',
});
};
expect(t).toThrow("Offline mode does not allow extensions to be installed");
});
it("writes settings to User settings path not Machine", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: "true",
offline: "true",
});
const instance = findResourceInstance(state, "coder_script");
// Verify the script uses User path, not Machine path
expect(instance.script).toContain(".vscode-server/data/User/settings.json");
expect(instance.script).not.toContain(
".vscode-server/data/Machine/settings.json",
);
});
it("writes provided settings to ~/.vscode-server/data/User/settings.json", async () => {
const id = await runContainer("alpine");
afterEach(async () => {
for (const id of cleanupContainers) {
try {
const settings = {
"editor.fontSize": 16,
"workbench.colorTheme": "Default Dark+",
};
await removeContainer(id);
} catch {
// Ignore cleanup errors
}
}
cleanupContainers = [];
});
describe("vscode-web", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
describe("terraform validation", () => {
it("accept_license should be set to true", async () => {
try {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: false,
});
throw new Error("Expected terraform apply to fail");
} catch (ex) {
expect((ex as Error).message).toContain("Invalid value for variable");
}
});
it("use_cached and offline can not be used together", async () => {
try {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
use_cached: true,
offline: true,
});
throw new Error("Expected terraform apply to fail");
} catch (ex) {
expect((ex as Error).message).toContain(
"Offline and Use Cached can not be used together",
);
}
});
it("offline and extensions can not be used together", async () => {
try {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
offline: true,
extensions: '["ms-python.python"]',
});
throw new Error("Expected terraform apply to fail");
} catch (ex) {
expect((ex as Error).message).toContain(
"Offline mode does not allow extensions to be installed",
);
}
});
it("workspace and folder can not be used together", async () => {
try {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
folder: "/home/coder",
workspace: "/home/coder/test.code-workspace",
});
throw new Error("Expected terraform apply to fail");
} catch (ex) {
expect((ex as Error).message).toContain(
"Set only one of `workspace` or `folder`",
);
}
});
});
describe("script generation", () => {
it("generates script with correct port", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: "true",
offline: "true",
accept_license: true,
port: 8080,
});
const script = findResourceInstance(state, "coder_script");
expect(script.script).toContain("--port 8080");
});
it("generates script with extensions directory", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
extensions_dir: "/custom/extensions",
});
const script = findResourceInstance(state, "coder_script");
expect(script.script).toContain("--extensions-dir=/custom/extensions");
});
it("generates script with telemetry level", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
telemetry_level: "off",
});
const script = findResourceInstance(state, "coder_script");
expect(script.script).toContain("--telemetry-level off");
});
it("generates script with disable trust", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
disable_trust: true,
});
const script = findResourceInstance(state, "coder_script");
expect(script.script).toContain("--disable-workspace-trust");
});
it("generates script with serve-web command", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
});
const script = findResourceInstance(state, "coder_script");
expect(script.script).toContain("serve-web");
expect(script.script).toContain("--accept-server-license-terms");
expect(script.script).toContain("--without-connection-token");
});
it("generates script with stable release channel by default", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
});
const script = findResourceInstance(state, "coder_script");
expect(script.script).toContain("build=stable");
});
it("generates script with insiders release channel", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
release_channel: "insiders",
});
const script = findResourceInstance(state, "coder_script");
expect(script.script).toContain("build=insiders");
});
it("generates script without commit-id value when not specified", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
});
const script = findResourceInstance(state, "coder_script");
// The if condition should have an empty string, so no commit-id value is passed
expect(script.script).toContain('if [ -n "" ]; then');
// Should not contain any actual commit hash
expect(script.script).not.toMatch(/--commit-id [a-f0-9]{40}/);
});
it("generates script with commit-id when specified", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
commit_id: "e54c774e0add60467559eb0d1e229c6452cf8447",
});
const script = findResourceInstance(state, "coder_script");
expect(script.script).toContain(
"--commit-id e54c774e0add60467559eb0d1e229c6452cf8447",
);
});
});
describe("container integration tests", () => {
it("uses existing code CLI in PATH", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Create a mock code CLI that logs when serve-web is called
await execContainer(containerId, [
"bash",
"-c",
`cat > /usr/local/bin/code << 'MOCKEOF'
#!/bin/bash
if [ "\$1" = "serve-web" ]; then
echo "MOCK_SERVER_STARTED with args: \$@"
exit 0
fi
echo "code mock called: \$@"
exit 0
MOCKEOF
chmod +x /usr/local/bin/code`,
]);
const script = findResourceInstance(state, "coder_script");
// Run the script - the mock will capture the serve-web call
const result = await execContainer(containerId, [
"bash",
"-c",
script.script,
]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("Found VS Code CLI");
});
it("offline mode fails when CLI not present", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
offline: true,
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
const script = findResourceInstance(state, "coder_script");
const result = await execContainer(containerId, [
"bash",
"-c",
script.script,
]);
expect(result.exitCode).toBe(1);
expect(result.stdout).toContain(
"Offline mode enabled but no VS Code CLI, code-server, or cached VS Code Server found",
);
});
it("offline mode uses code-server as fallback", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
offline: true,
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Install mock code-server in PATH
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code-server << 'MOCKEOF'
#!/bin/bash
echo "MOCK_CODE_SERVER_STARTED with args: $@"
exit 0
MOCKEOF
chmod +x /usr/local/bin/code-server`,
]);
const script = findResourceInstance(state, "coder_script");
const result = await execContainer(containerId, [
"bash",
"-c",
script.script,
]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("offline fallback");
expect(result.stdout).toContain("Starting code-server");
});
it("offline mode works with pre-installed CLI", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
offline: true,
install_prefix: "/tmp/vscode-web",
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Pre-install mock code CLI at expected location
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code << 'MOCKEOF'
#!/bin/bash
if [ "\$1" = "serve-web" ]; then
echo "MOCK_OFFLINE_SERVER_STARTED"
exit 0
fi
exit 0
MOCKEOF
chmod +x /tmp/vscode-web/bin/code`,
]);
const script = findResourceInstance(state, "coder_script");
const result = await execContainer(containerId, [
"bash",
"-c",
script.script,
]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("Using cached VS Code CLI");
expect(result.stdout).toContain("Starting VS Code Web");
});
it("use_cached mode works with pre-installed CLI", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
use_cached: true,
install_prefix: "/tmp/vscode-web",
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Pre-install mock code CLI
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code << 'MOCKEOF'
#!/bin/bash
if [ "\$1" = "serve-web" ]; then
echo "MOCK_CACHED_SERVER_STARTED"
exit 0
fi
exit 0
MOCKEOF
chmod +x /tmp/vscode-web/bin/code`,
]);
const script = findResourceInstance(state, "coder_script");
const result = await execContainer(containerId, [
"bash",
"-c",
script.script,
]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("Using cached VS Code CLI");
});
it("creates settings file with correct content", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
settings: '{"editor.fontSize": 14}',
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Create a mock code CLI
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
#!/bin/bash
exit 0
MOCKEOF
chmod +x /usr/local/bin/code`,
]);
const script = findResourceInstance(state, "coder_script");
await execContainer(containerId, ["bash", "-c", script.script]);
// Check that settings file was created
const settingsResult = await execContainer(containerId, [
"cat",
"/root/.vscode-server/data/Machine/settings.json",
]);
expect(settingsResult.exitCode).toBe(0);
expect(settingsResult.stdout).toContain("editor.fontSize");
expect(settingsResult.stdout).toContain("14");
});
it("creates settings file with multiple settings", async () => {
const settings = {
"editor.fontSize": 16,
"editor.tabSize": 2,
"workbench.colorTheme": "Dracula",
"editor.formatOnSave": true,
};
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
settings: JSON.stringify(settings),
});
const instance = findResourceInstance(state, "coder_script");
// Extract and run only the settings portion of the script
const settingsScript = `
SETTINGS='${JSON.stringify(settings).replace(/'/g, "'\\''")}'
if [ ! -f ~/.vscode-server/data/User/settings.json ]; then
mkdir -p ~/.vscode-server/data/User
echo "$SETTINGS" > ~/.vscode-server/data/User/settings.json
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Create a mock code CLI
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
#!/bin/bash
exit 0
MOCKEOF
chmod +x /usr/local/bin/code`,
]);
const script = findResourceInstance(state, "coder_script");
await execContainer(containerId, ["bash", "-c", script.script]);
// Check that settings file was created with all settings
const settingsResult = await execContainer(containerId, [
"cat",
"/root/.vscode-server/data/Machine/settings.json",
]);
expect(settingsResult.exitCode).toBe(0);
expect(settingsResult.stdout).toContain("editor.fontSize");
expect(settingsResult.stdout).toContain("16");
expect(settingsResult.stdout).toContain("editor.tabSize");
expect(settingsResult.stdout).toContain("2");
expect(settingsResult.stdout).toContain("workbench.colorTheme");
expect(settingsResult.stdout).toContain("Dracula");
expect(settingsResult.stdout).toContain("editor.formatOnSave");
expect(settingsResult.stdout).toContain("true");
});
it("creates settings file in correct directory structure", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
settings: '{"test.setting": "value"}',
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Create a mock code CLI
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
#!/bin/bash
exit 0
MOCKEOF
chmod +x /usr/local/bin/code`,
]);
const script = findResourceInstance(state, "coder_script");
await execContainer(containerId, ["bash", "-c", script.script]);
// Verify directory structure was created
const dirResult = await execContainer(containerId, [
"ls",
"-la",
"/root/.vscode-server/data/Machine/",
]);
expect(dirResult.exitCode).toBe(0);
expect(dirResult.stdout).toContain("settings.json");
// Verify parent directories exist
const parentDirResult = await execContainer(containerId, [
"ls",
"-la",
"/root/.vscode-server/data/",
]);
expect(parentDirResult.exitCode).toBe(0);
expect(parentDirResult.stdout).toContain("Machine");
});
it("merges settings with existing settings file", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
settings: '{"new.setting": "new_value"}',
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Install jq and create mock code CLI
await execContainer(containerId, ["apt-get", "update", "-qq"]);
await execContainer(containerId, [
"apt-get",
"install",
"-y",
"-qq",
"jq",
]);
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
#!/bin/bash
exit 0
MOCKEOF
chmod +x /usr/local/bin/code`,
]);
// Pre-create an existing settings file
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /root/.vscode-server/data/Machine && echo '{"existing.setting": "existing_value"}' > /root/.vscode-server/data/Machine/settings.json`,
]);
const script = findResourceInstance(state, "coder_script");
await execContainer(containerId, ["bash", "-c", script.script]);
// Check that settings were merged (both existing and new should be present)
const settingsResult = await execContainer(containerId, [
"cat",
"/root/.vscode-server/data/Machine/settings.json",
]);
expect(settingsResult.exitCode).toBe(0);
// Should contain both existing and new settings
expect(settingsResult.stdout).toContain("existing.setting");
expect(settingsResult.stdout).toContain("existing_value");
expect(settingsResult.stdout).toContain("new.setting");
expect(settingsResult.stdout).toContain("new_value");
});
it("creates valid JSON settings file", async () => {
const settings = {
"editor.fontSize": 14,
"editor.wordWrap": "on",
"files.autoSave": "afterDelay",
"files.autoSaveDelay": 1000,
};
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
settings: JSON.stringify(settings),
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Install jq and create mock code CLI
await execContainer(containerId, ["apt-get", "update", "-qq"]);
await execContainer(containerId, [
"apt-get",
"install",
"-y",
"-qq",
"jq",
]);
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
#!/bin/bash
exit 0
MOCKEOF
chmod +x /usr/local/bin/code`,
]);
const script = findResourceInstance(state, "coder_script");
await execContainer(containerId, ["bash", "-c", script.script]);
// Validate JSON using jq
const jsonValidResult = await execContainer(containerId, [
"bash",
"-c",
"jq '.' /root/.vscode-server/data/Machine/settings.json",
]);
expect(jsonValidResult.exitCode).toBe(0);
// Extract specific values using jq
const fontSizeResult = await execContainer(containerId, [
"bash",
"-c",
"jq '.\"editor.fontSize\"' /root/.vscode-server/data/Machine/settings.json",
]);
expect(fontSizeResult.stdout.trim()).toBe("14");
const wordWrapResult = await execContainer(containerId, [
"bash",
"-c",
"jq '.\"editor.wordWrap\"' /root/.vscode-server/data/Machine/settings.json",
]);
expect(wordWrapResult.stdout.trim()).toBe('"on"');
const autoSaveDelayResult = await execContainer(containerId, [
"bash",
"-c",
"jq '.\"files.autoSaveDelay\"' /root/.vscode-server/data/Machine/settings.json",
]);
expect(autoSaveDelayResult.stdout.trim()).toBe("1000");
});
it("installs extensions", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
extensions: '["ms-python.python", "golang.go"]',
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Create a mock code CLI that logs extension installs
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
#!/bin/bash
if [ "\$1" = "--install-extension" ]; then
echo "MOCK_EXTENSION_INSTALL: \$2"
fi
`;
const resp = await execContainer(id, ["sh", "-c", settingsScript]);
expect(resp.exitCode).toBe(0);
const content = await readFileContainer(
id,
"/root/.vscode-server/data/User/settings.json",
);
const actualSettings = JSON.parse(content.trim());
expect(actualSettings).toEqual(settings);
} finally {
await removeContainer(id);
}
exit 0
MOCKEOF
chmod +x /usr/local/bin/code`,
]);
const script = findResourceInstance(state, "coder_script");
const result = await execContainer(containerId, [
"bash",
"-c",
script.script,
]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("Installing extension");
expect(result.stdout).toContain("ms-python.python");
expect(result.stdout).toContain("golang.go");
});
it("runs with correct server arguments", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
port: 9999,
telemetry_level: "off",
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Create a mock code CLI that captures all arguments
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
#!/bin/bash
echo "MOCK_CODE_ARGS: \$@"
exit 0
MOCKEOF
chmod +x /usr/local/bin/code`,
]);
const script = findResourceInstance(state, "coder_script");
const result = await execContainer(containerId, [
"bash",
"-c",
script.script,
]);
expect(result.exitCode).toBe(0);
// Check the output contains expected port message
expect(result.stdout).toContain("Starting VS Code Web on port 9999");
});
it("passes commit-id to code CLI when specified", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
commit_id: "abc123def456",
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Create a mock code CLI that logs arguments to the log file (where output is redirected)
await execContainer(containerId, [
"bash",
"-c",
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
#!/bin/bash
echo "MOCK_CODE_ARGS: $@"
exit 0
MOCKEOF
chmod +x /usr/local/bin/code`,
]);
const script = findResourceInstance(state, "coder_script");
await execContainer(containerId, ["bash", "-c", script.script]);
// Wait briefly for background process to write to log
await new Promise((resolve) => setTimeout(resolve, 500));
// Check the log file for the arguments (code CLI output goes there)
const logResult = await execContainer(containerId, [
"cat",
"/tmp/vscode-web.log",
]);
expect(logResult.exitCode).toBe(0);
expect(logResult.stdout).toContain("--commit-id abc123def456");
});
// This test downloads and starts the real VS Code server
it("starts real VS Code CLI and responds to healthcheck (requires network)", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: true,
port: 13338,
install_prefix: "/tmp/vscode-web",
});
const containerId = await runContainer("ubuntu:22.04");
cleanupContainers.push(containerId);
// Install curl for downloading CLI and healthcheck
await execContainer(containerId, ["apt-get", "update", "-qq"]);
await execContainer(containerId, [
"apt-get",
"install",
"-y",
"-qq",
"curl",
]);
const script = findResourceInstance(state, "coder_script");
// Run the script - it will start the server in background
const startResult = await execContainer(containerId, [
"bash",
"-c",
script.script,
]);
expect(startResult.exitCode).toBe(0);
expect(startResult.stdout).toContain("Starting VS Code Web");
// Wait for server to start and check healthcheck
await new Promise((resolve) => setTimeout(resolve, 10000));
const healthResult = await execContainer(containerId, [
"curl",
"-s",
"-o",
"/dev/null",
"-w",
"%{http_code}",
"http://127.0.0.1:13338/healthz",
]);
// Server should respond (200, 202, or 404 is acceptable - means server is running)
expect(["200", "202", "404"]).toContain(healthResult.stdout.trim());
});
});
});
+23 -20
View File
@@ -59,12 +59,6 @@ variable "install_prefix" {
default = "/tmp/vscode-web"
}
variable "commit_id" {
type = string
description = "Specify the commit ID of the VS Code Web binary to pin to a specific version. If left empty, the latest stable version is used."
default = ""
}
variable "extensions" {
type = list(string)
description = "A list of extensions to install."
@@ -105,7 +99,7 @@ variable "group" {
variable "settings" {
type = any
description = "A map of settings to apply to VS Code web."
description = "A map of settings to apply to VS Code Web's Machine settings. These settings are merged with any existing machine settings on startup."
default = {}
}
@@ -148,25 +142,35 @@ variable "subdomain" {
default = true
}
variable "platform" {
type = string
description = "The platform to use for the VS Code Web."
default = ""
validation {
condition = var.platform == "" || var.platform == "linux" || var.platform == "darwin" || var.platform == "alpine" || var.platform == "win32"
error_message = "Incorrect value. Please set either 'linux', 'darwin', or 'alpine' or 'win32'."
}
}
variable "workspace" {
type = string
description = "Path to a .code-workspace file to open in vscode-web."
default = ""
}
variable "release_channel" {
type = string
description = "The release channel for VS Code CLI (stable or insiders)."
default = "stable"
validation {
condition = var.release_channel == "stable" || var.release_channel == "insiders"
error_message = "Incorrect value. Please set either 'stable' or 'insiders'."
}
}
variable "commit_id" {
type = string
description = "The commit SHA to use for the VS Code Server. Leave empty to use the latest version."
default = ""
}
data "coder_workspace_owner" "me" {}
data "coder_workspace" "me" {}
locals {
settings_b64 = var.settings != {} ? base64encode(jsonencode(var.settings)) : ""
}
resource "coder_script" "vscode-web" {
agent_id = var.agent_id
display_name = "VS Code Web"
@@ -177,8 +181,7 @@ resource "coder_script" "vscode-web" {
INSTALL_PREFIX : var.install_prefix,
EXTENSIONS : join(",", var.extensions),
TELEMETRY_LEVEL : var.telemetry_level,
// This is necessary otherwise the quotes are stripped!
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
SETTINGS_B64 : local.settings_b64,
OFFLINE : var.offline,
USE_CACHED : var.use_cached,
DISABLE_TRUST : var.disable_trust,
@@ -187,8 +190,8 @@ resource "coder_script" "vscode-web" {
WORKSPACE : var.workspace,
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
SERVER_BASE_PATH : local.server_base_path,
RELEASE_CHANNEL : var.release_channel,
COMMIT_ID : var.commit_id,
PLATFORM : var.platform,
})
run_on_start = true
+384 -105
View File
@@ -1,138 +1,417 @@
#!/usr/bin/env bash
BOLD='\033[0;1m'
RESET='\033[0m'
CODE='\033[36;40;1m'
EXTENSIONS=("${EXTENSIONS}")
VSCODE_WEB="${INSTALL_PREFIX}/bin/code-server"
# Set extension directory
# Merge settings from module with existing settings file
# Uses jq if available, falls back to Python3 for deep merge
merge_settings() {
local new_settings="$1"
local settings_file="$2"
if [ -z "$new_settings" ] || [ "$new_settings" = "{}" ]; then
return 0
fi
if [ ! -f "$settings_file" ]; then
mkdir -p "$(dirname "$settings_file")"
printf '%s\n' "$new_settings" > "$settings_file"
printf "Creating settings file...\n"
return 0
fi
local tmpfile
tmpfile="$(mktemp)"
if command -v jq > /dev/null 2>&1; then
if jq -s '.[0] * .[1]' "$settings_file" <(printf '%s\n' "$new_settings") > "$tmpfile" 2> /dev/null; then
mv "$tmpfile" "$settings_file"
printf "Merging settings...\n"
return 0
fi
fi
if command -v python3 > /dev/null 2>&1; then
if python3 -c "import json,sys;m=lambda a,b:{**a,**{k:m(a[k],v)if k in a and type(a[k])==type(v)==dict else v for k,v in b.items()}};print(json.dumps(m(json.load(open(sys.argv[1])),json.loads(sys.argv[2])),indent=2))" "$settings_file" "$new_settings" > "$tmpfile" 2> /dev/null; then
mv "$tmpfile" "$settings_file"
printf "Merging settings...\n"
return 0
fi
fi
rm -f "$tmpfile"
printf "Warning: Could not merge settings. Keeping existing settings.\n"
return 0
}
# Set extension directory argument
EXTENSION_ARG=""
if [ -n "${EXTENSIONS_DIR}" ]; then
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
fi
# Set extension directory
# Set server base path argument
SERVER_BASE_PATH_ARG=""
if [ -n "${SERVER_BASE_PATH}" ]; then
SERVER_BASE_PATH_ARG="--server-base-path=${SERVER_BASE_PATH}"
fi
# Set disable workspace trust
# Set disable workspace trust argument
DISABLE_TRUST_ARG=""
if [ "${DISABLE_TRUST}" = true ]; then
DISABLE_TRUST_ARG="--disable-workspace-trust"
fi
run_vscode_web() {
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG $DISABLE_TRUST_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
echo "Check logs at ${LOG_PATH}!"
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" "$DISABLE_TRUST_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
# Check if code CLI is installed
check_code_cli() {
if command -v code > /dev/null 2>&1; then
echo "code"
return 0
fi
if [ -f "${INSTALL_PREFIX}/bin/code" ]; then
echo "${INSTALL_PREFIX}/bin/code"
return 0
fi
return 1
}
# Check if the settings file exists...
if [ ! -f ~/.vscode-server/data/User/settings.json ]; then
echo "⚙️ Creating settings file..."
mkdir -p ~/.vscode-server/data/User
echo "${SETTINGS}" > ~/.vscode-server/data/User/settings.json
fi
# Check if vscode-server is already installed for offline or cached mode
if [ -f "$VSCODE_WEB" ]; then
if [ "${OFFLINE}" = true ] || [ "${USE_CACHED}" = true ]; then
echo "🥳 Found a copy of VS Code Web"
run_vscode_web
exit 0
# Check if code-server is installed (fallback option)
check_code_server() {
if command -v code-server > /dev/null 2>&1; then
echo "code-server"
return 0
fi
fi
# Offline mode always expects a copy of vscode-server to be present
if [ "${OFFLINE}" = true ]; then
echo "Failed to find a copy of VS Code Web"
exit 1
fi
# Create install prefix
mkdir -p ${INSTALL_PREFIX}
printf "$${BOLD}Installing Microsoft Visual Studio Code Server!\n"
# Download and extract vscode-server
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="x64" ;;
aarch64) ARCH="arm64" ;;
*)
echo "Unsupported architecture"
exit 1
;;
esac
# Detect the platform
if [ -n "${PLATFORM}" ]; then
DETECTED_PLATFORM="${PLATFORM}"
elif [ -f /etc/alpine-release ] || grep -qi 'ID=alpine' /etc/os-release 2> /dev/null || command -v apk > /dev/null 2>&1; then
DETECTED_PLATFORM="alpine"
elif [ "$(uname -s)" = "Darwin" ]; then
DETECTED_PLATFORM="darwin"
else
DETECTED_PLATFORM="linux"
fi
# Check if a specific VS Code Web commit ID was provided
if [ -n "${COMMIT_ID}" ]; then
HASH="${COMMIT_ID}"
else
HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-$DETECTED_PLATFORM-$ARCH-web | cut -d '"' -f 2)
fi
printf "$${BOLD}VS Code Web commit id version $HASH.\n"
output=$(curl -fsSL "https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-$DETECTED_PLATFORM-$ARCH-web.tar.gz" | tar -xz -C "${INSTALL_PREFIX}" --strip-components 1)
if [ $? -ne 0 ]; then
echo "Failed to install Microsoft Visual Studio Code Server: $output"
exit 1
fi
printf "$${BOLD}VS Code Web has been installed.\n"
# Install each extension...
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
# shellcheck disable=SC2066
for extension in "$${EXTENSIONLIST[@]}"; do
if [ -z "$extension" ]; then
continue
if [ -f "${INSTALL_PREFIX}/bin/code-server" ]; then
echo "${INSTALL_PREFIX}/bin/code-server"
return 0
fi
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
output=$($VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force)
if [ $? -ne 0 ]; then
echo "Failed to install extension: $extension: $output"
fi
done
return 1
}
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
if ! command -v jq > /dev/null; then
echo "jq is required to install extensions from a workspace file."
else
# Prefer WORKSPACE if set and points to a file
if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then
printf "🧩 Installing extensions from %s...\n" "${WORKSPACE}"
# Strip single-line comments then parse .extensions.recommendations[]
extensions=$(sed 's|//.*||g' "${WORKSPACE}" | jq -r '(.extensions.recommendations // [])[]')
for extension in $extensions; do
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
done
else
# Fallback to folder-based .vscode/extensions.json (existing behavior)
WORKSPACE_DIR="$HOME"
if [ -n "${FOLDER}" ]; then
WORKSPACE_DIR="${FOLDER}"
# Find existing vscode-server binary (used by code serve-web internally)
find_vscode_server() {
# Check common locations for pre-downloaded vscode-server
local server_dirs=(
"$HOME/.vscode-server/bin"
"$HOME/.vscode/cli/serve-web"
)
for dir in "$${server_dirs[@]}"; do
if [ -d "$dir" ]; then
# Find the most recent server version
local latest
latest=$(ls -t "$dir" 2> /dev/null | head -1)
if [ -n "$latest" ] && [ -f "$dir/$latest/bin/code-server" ]; then
echo "$dir/$latest/bin/code-server"
return 0
fi
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR/.vscode/extensions.json" | jq -r '.recommendations[]')
if [ -n "$latest" ] && [ -f "$dir/$latest/code-server" ]; then
echo "$dir/$latest/code-server"
return 0
fi
fi
done
return 1
}
# Install VS Code CLI if not present
install_code_cli() {
printf "$${BOLD}Installing VS Code CLI...$${RESET}\n"
# Detect architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="x64" ;;
aarch64 | arm64) ARCH="arm64" ;;
armv7l) ARCH="armhf" ;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac
# Detect platform
# Note: VS Code CLI uses 'alpine' for all Linux distributions
PLATFORM=$(uname -s)
case "$PLATFORM" in
Linux)
PLATFORM="alpine"
;;
Darwin)
PLATFORM="darwin"
;;
*)
echo "Unsupported platform: $PLATFORM"
exit 1
;;
esac
# Create install directory
mkdir -p "${INSTALL_PREFIX}/bin"
# Download VS Code CLI
CLI_URL="https://code.visualstudio.com/sha/download?build=${RELEASE_CHANNEL}&os=cli-$PLATFORM-$ARCH"
printf "Downloading VS Code CLI from %s\n" "$CLI_URL"
if command -v curl > /dev/null 2>&1; then
curl -fsSL "$CLI_URL" -o "/tmp/vscode-cli.tar.gz"
elif command -v wget > /dev/null 2>&1; then
wget -q "$CLI_URL" -O "/tmp/vscode-cli.tar.gz"
else
echo "Neither curl nor wget is available. Please install one of them."
exit 1
fi
# Extract CLI
tar -xzf /tmp/vscode-cli.tar.gz -C "${INSTALL_PREFIX}/bin"
rm -f /tmp/vscode-cli.tar.gz
# The CLI binary is named 'code'
if [ -f "${INSTALL_PREFIX}/bin/code" ]; then
chmod +x "${INSTALL_PREFIX}/bin/code"
export PATH="${INSTALL_PREFIX}/bin:$PATH"
printf "$${BOLD}VS Code CLI installed successfully.$${RESET}\n"
else
echo "Failed to install VS Code CLI"
exit 1
fi
}
# Run VS Code Web using the code CLI (serve-web command)
run_vscode_web_cli() {
local CODE_CMD="$1"
# Build the command arguments
ARGS="serve-web --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL}"
if [ -n "$EXTENSION_ARG" ]; then
ARGS="$ARGS $EXTENSION_ARG"
fi
if [ -n "$SERVER_BASE_PATH_ARG" ]; then
ARGS="$ARGS $SERVER_BASE_PATH_ARG"
fi
if [ -n "$DISABLE_TRUST_ARG" ]; then
ARGS="$ARGS $DISABLE_TRUST_ARG"
fi
if [ -n "${COMMIT_ID}" ]; then
ARGS="$ARGS --commit-id ${COMMIT_ID}"
fi
printf "Starting VS Code Web on port ${PORT}...\n"
printf "Check logs at ${LOG_PATH}\n"
# shellcheck disable=SC2086
"$CODE_CMD" $ARGS > "${LOG_PATH}" 2>&1 &
}
# Run VS Code Web using code-server (fallback for offline mode)
run_code_server() {
local SERVER_CMD="$1"
printf "Starting code-server on port ${PORT}...\n"
printf "Check logs at ${LOG_PATH}\n"
# Build arguments for code-server
ARGS="--port ${PORT} --host 127.0.0.1 --auth none"
if [ -n "$EXTENSION_ARG" ]; then
ARGS="$ARGS $EXTENSION_ARG"
fi
# shellcheck disable=SC2086
"$SERVER_CMD" $ARGS > "${LOG_PATH}" 2>&1 &
}
# Run VS Code Web using vscode-server binary directly
run_vscode_server() {
local SERVER_CMD="$1"
printf "Starting VS Code Server on port ${PORT}...\n"
printf "Check logs at ${LOG_PATH}\n"
# Build arguments for vscode-server
ARGS="--port ${PORT} --host 127.0.0.1 --without-connection-token --accept-server-license-terms --telemetry-level ${TELEMETRY_LEVEL}"
if [ -n "$EXTENSION_ARG" ]; then
ARGS="$ARGS $EXTENSION_ARG"
fi
if [ -n "$SERVER_BASE_PATH_ARG" ]; then
ARGS="$ARGS $SERVER_BASE_PATH_ARG"
fi
# shellcheck disable=SC2086
"$SERVER_CMD" serve-local $ARGS > "${LOG_PATH}" 2>&1 &
}
# Install a single extension by downloading VSIX from marketplace
install_extension_vsix() {
local ext_id="$1"
local publisher
local ext_name
publisher="$${ext_id%%.*}"
ext_name="$${ext_id#*.}"
# Download VSIX from marketplace
local vsix_url="https://$publisher.gallery.vsassets.io/_apis/public/gallery/publisher/$publisher/extension/$ext_name/latest/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage"
local tmp_vsix="/tmp/ext-$ext_id.vsix"
local tmp_dir="/tmp/ext-$ext_id"
if command -v curl > /dev/null 2>&1; then
curl -fsSL "$vsix_url" -o "$tmp_vsix" 2> /dev/null
elif command -v wget > /dev/null 2>&1; then
wget -q "$vsix_url" -O "$tmp_vsix" 2> /dev/null
else
echo "Failed to install extension $ext_id: neither curl nor wget available"
return 1
fi
if [ ! -f "$tmp_vsix" ]; then
echo "Failed to download extension: $ext_id"
return 1
fi
# Extract VSIX (it's a ZIP file)
rm -rf "$tmp_dir"
mkdir -p "$tmp_dir"
if ! unzip -q "$tmp_vsix" -d "$tmp_dir" 2> /dev/null; then
echo "Failed to extract extension: $ext_id"
rm -f "$tmp_vsix"
return 1
fi
# Get version from package.json
local version=""
if [ -f "$tmp_dir/extension/package.json" ]; then
version=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$tmp_dir/extension/package.json" | head -1 | cut -d'"' -f4)
fi
if [ -z "$version" ]; then
version="0.0.0"
fi
# Install to extensions directory
local ext_dir="$HOME/.vscode-server/extensions/$ext_id-$version"
mkdir -p "$HOME/.vscode-server/extensions"
rm -rf "$ext_dir"
mv "$tmp_dir/extension" "$ext_dir"
# Cleanup
rm -rf "$tmp_vsix" "$tmp_dir"
printf "Extension $ext_id v$version installed successfully.\n"
return 0
}
install_extensions() {
# Install specified extensions
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
for extension in "$${EXTENSIONLIST[@]}"; do
if [ -z "$extension" ]; then
continue
fi
printf "Installing extension $extension...\n"
install_extension_vsix "$extension"
done
# Auto-install extensions from workspace or folder
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
if ! command -v jq > /dev/null; then
echo "jq is required to install extensions from a workspace file."
else
if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then
printf "Installing extensions from %s...\n" "${WORKSPACE}"
extensions=$(sed 's|//.*||g' "${WORKSPACE}" | jq -r '(.extensions.recommendations // [])[]')
for extension in $extensions; do
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
install_extension_vsix "$extension"
done
else
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=$(sed 's|//.*||g' "$WORKSPACE_DIR/.vscode/extensions.json" | jq -r '.recommendations[]')
for extension in $extensions; do
install_extension_vsix "$extension"
done
fi
fi
fi
fi
}
# Apply machine settings (merge with existing if present)
SETTINGS_B64='${SETTINGS_B64}'
if [ -n "$SETTINGS_B64" ]; then
SETTINGS_JSON="$(echo -n "$SETTINGS_B64" | base64 -d)"
merge_settings "$SETTINGS_JSON" ~/.vscode-server/data/Machine/settings.json
fi
run_vscode_web
# Determine which command to use
CODE_CMD=""
RUN_MODE=""
# Check for code CLI first (preferred)
if CODE_CMD=$(check_code_cli); then
printf "$${BOLD}Found VS Code CLI at $CODE_CMD$${RESET}\n"
RUN_MODE="cli"
fi
# Handle offline mode
if [ "${OFFLINE}" = true ]; then
if [ -n "$CODE_CMD" ]; then
# Check if vscode-server is already downloaded (code serve-web won't need to download)
if VSCODE_SERVER=$(find_vscode_server); then
printf "Found cached VS Code Server at $VSCODE_SERVER\n"
printf "Using cached VS Code CLI.\n"
run_vscode_web_cli "$CODE_CMD"
exit 0
fi
# Code CLI exists but vscode-server not cached - try using it anyway
# (it might work if server was pre-downloaded, or fail gracefully)
printf "Warning: VS Code Server may not be cached. Attempting to start...\n"
printf "Using cached VS Code CLI.\n"
run_vscode_web_cli "$CODE_CMD"
exit 0
fi
# Try code-server as fallback for offline mode
if SERVER_CMD=$(check_code_server); then
printf "$${BOLD}Found code-server at $SERVER_CMD (offline fallback)$${RESET}\n"
run_code_server "$SERVER_CMD"
exit 0
fi
# Try vscode-server binary directly
if VSCODE_SERVER=$(find_vscode_server); then
printf "$${BOLD}Found VS Code Server at $VSCODE_SERVER (offline fallback)$${RESET}\n"
run_vscode_server "$VSCODE_SERVER"
exit 0
fi
echo "Offline mode enabled but no VS Code CLI, code-server, or cached VS Code Server found."
exit 1
fi
# Handle use_cached mode
if [ "${USE_CACHED}" = true ] && [ -n "$CODE_CMD" ]; then
printf "Using cached VS Code CLI.\n"
install_extensions
run_vscode_web_cli "$CODE_CMD"
exit 0
fi
# Install VS Code CLI if not present
if [ -z "$CODE_CMD" ]; then
install_code_cli
CODE_CMD="${INSTALL_PREFIX}/bin/code"
RUN_MODE="cli"
fi
# Install extensions and run VS Code Web
install_extensions
run_vscode_web_cli "$CODE_CMD"
@@ -0,0 +1,151 @@
run "required_vars" {
command = plan
variables {
agent_id = "foo"
accept_license = true
}
}
run "accept_license_required" {
command = plan
variables {
agent_id = "foo"
accept_license = false
}
expect_failures = [
var.accept_license
]
}
run "offline_and_use_cached_conflict" {
command = plan
variables {
agent_id = "foo"
accept_license = true
use_cached = true
offline = true
}
expect_failures = [
resource.coder_script.vscode-web
]
}
run "offline_disallows_extensions" {
command = plan
variables {
agent_id = "foo"
accept_license = true
offline = true
extensions = ["ms-python.python", "golang.go"]
}
expect_failures = [
resource.coder_script.vscode-web
]
}
run "workspace_and_folder_conflict" {
command = plan
variables {
agent_id = "foo"
accept_license = true
folder = "/home/coder/project"
workspace = "/home/coder/project.code-workspace"
}
expect_failures = [
resource.coder_script.vscode-web
]
}
run "url_with_folder_query" {
command = plan
variables {
agent_id = "foo"
accept_license = true
folder = "/home/coder/project"
port = 13338
}
assert {
condition = resource.coder_app.vscode-web.url == "http://localhost:13338?folder=%2Fhome%2Fcoder%2Fproject"
error_message = "coder_app URL must include encoded folder query param"
}
}
run "url_with_workspace_query" {
command = plan
variables {
agent_id = "foo"
accept_license = true
workspace = "/home/coder/project.code-workspace"
port = 13338
}
assert {
condition = resource.coder_app.vscode-web.url == "http://localhost:13338?workspace=%2Fhome%2Fcoder%2Fproject.code-workspace"
error_message = "coder_app URL must include encoded workspace query param"
}
}
run "release_channel_stable" {
command = plan
variables {
agent_id = "foo"
accept_license = true
release_channel = "stable"
}
}
run "release_channel_insiders" {
command = plan
variables {
agent_id = "foo"
accept_license = true
release_channel = "insiders"
}
}
run "release_channel_invalid" {
command = plan
variables {
agent_id = "foo"
accept_license = true
release_channel = "invalid"
}
expect_failures = [
var.release_channel
]
}
run "commit_id_empty_by_default" {
command = plan
variables {
agent_id = "foo"
accept_license = true
}
}
run "commit_id_with_value" {
command = plan
variables {
agent_id = "foo"
accept_license = true
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
}
}