From 2c471683acb4adb597640aff6db4ac7c039f6ced Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Thu, 4 Dec 2025 21:13:53 +0300 Subject: [PATCH 1/6] Add configurable autostart behavior for SSH connections --- package.json | 15 ++++ src/api/workspace.ts | 2 +- src/cliConfig.ts | 40 +++++++++++ src/commands.ts | 2 +- src/globalFlags.ts | 20 ------ src/remote/remote.ts | 11 ++- test/unit/cliConfig.test.ts | 132 ++++++++++++++++++++++++++++++++++ test/unit/globalFlags.test.ts | 88 ----------------------- 8 files changed, 198 insertions(+), 112 deletions(-) create mode 100644 src/cliConfig.ts delete mode 100644 src/globalFlags.ts create mode 100644 test/unit/cliConfig.test.ts delete mode 100644 test/unit/globalFlags.test.ts diff --git a/package.json b/package.json index bd60a54c..7685dffb 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,21 @@ "type": "boolean", "default": false }, + "coder.disableAutostart": { + "markdownDescription": "Disable starting the workspace automatically when connecting via SSH.", + "type": "string", + "enum": [ + "auto", + "always", + "never" + ], + "markdownEnumDescriptions": [ + "Disables autostart on macOS only (recommended to avoid sleep/wake issues)", + "Disables on all platforms", + "Keeps autostart enabled on all platforms" + ], + "default": "auto" + }, "coder.globalFlags": { "markdownDescription": "Global flags to pass to every Coder CLI invocation. Enter each flag as a separate array item; values are passed verbatim and in order. Do **not** include the `coder` command itself. See the [CLI reference](https://coder.com/docs/reference/cli) for available global flags.\n\nNote that for `--header-command`, precedence is: `#coder.headerCommand#` setting, then `CODER_HEADER_COMMAND` environment variable, then the value specified here. The `--global-config` flag is explicitly ignored.", "type": "array", diff --git a/src/api/workspace.ts b/src/api/workspace.ts index 1d3b7a4e..93319337 100644 --- a/src/api/workspace.ts +++ b/src/api/workspace.ts @@ -8,8 +8,8 @@ import { import { spawn } from "node:child_process"; import * as vscode from "vscode"; +import { getGlobalFlags } from "../cliConfig"; import { type FeatureSet } from "../featureSet"; -import { getGlobalFlags } from "../globalFlags"; import { escapeCommandArg } from "../util"; import { type UnidirectionalStream } from "../websocket/eventStreamConnection"; diff --git a/src/cliConfig.ts b/src/cliConfig.ts new file mode 100644 index 00000000..4d8c9000 --- /dev/null +++ b/src/cliConfig.ts @@ -0,0 +1,40 @@ +import { type WorkspaceConfiguration } from "vscode"; + +import { getHeaderArgs } from "./headers"; +import { escapeCommandArg } from "./util"; + +/** + * Returns global configuration flags for Coder CLI commands. + * Always includes the `--global-config` argument with the specified config directory. + */ +export function getGlobalFlags( + configs: WorkspaceConfiguration, + configDir: string, +): string[] { + // Last takes precedence/overrides previous ones + return [ + ...(configs.get("coder.globalFlags") || []), + "--global-config", + escapeCommandArg(configDir), + ...getHeaderArgs(configs), + ]; +} + +type DisableAutostartSetting = "auto" | "always" | "never"; + +/** + * Determines whether autostart should be disabled based on the setting and platform. + * - "always": disable on all platforms + * - "never": never disable + * - "auto": disable only on macOS (due to sleep/wake issues) + */ +export function shouldDisableAutostart( + configs: WorkspaceConfiguration, + platform: NodeJS.Platform, +): boolean { + const setting = configs.get( + "coder.disableAutostart", + "auto", + ); + return setting === "always" || (setting === "auto" && platform === "darwin"); +} diff --git a/src/commands.ts b/src/commands.ts index 384b4d79..9bb2ed54 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -10,6 +10,7 @@ import * as vscode from "vscode"; import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper"; import { CoderApi } from "./api/coderApi"; import { needToken } from "./api/utils"; +import { getGlobalFlags } from "./cliConfig"; import { type CliManager } from "./core/cliManager"; import { type ServiceContainer } from "./core/container"; import { type ContextManager } from "./core/contextManager"; @@ -17,7 +18,6 @@ import { type MementoManager } from "./core/mementoManager"; import { type PathResolver } from "./core/pathResolver"; import { type SecretsManager } from "./core/secretsManager"; import { CertificateError } from "./error"; -import { getGlobalFlags } from "./globalFlags"; import { type Logger } from "./logging/logger"; import { maybeAskAgent, maybeAskUrl } from "./promptUtils"; import { escapeCommandArg, toRemoteAuthority, toSafeHost } from "./util"; diff --git a/src/globalFlags.ts b/src/globalFlags.ts deleted file mode 100644 index 8e75ce8d..00000000 --- a/src/globalFlags.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { type WorkspaceConfiguration } from "vscode"; - -import { getHeaderArgs } from "./headers"; -import { escapeCommandArg } from "./util"; - -/** - * Returns global configuration flags for Coder CLI commands. - * Always includes the `--global-config` argument with the specified config directory. - */ -export function getGlobalFlags( - configs: WorkspaceConfiguration, - configDir: string, -): string[] { - // Last takes precedence/overrides previous ones - return [ - ...(configs.get("coder.globalFlags") || []), - ...["--global-config", escapeCommandArg(configDir)], - ...getHeaderArgs(configs), - ]; -} diff --git a/src/remote/remote.ts b/src/remote/remote.ts index 4193e46d..201d9fd3 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -20,6 +20,7 @@ import { import { extractAgents } from "../api/api-helper"; import { CoderApi } from "../api/coderApi"; import { needToken } from "../api/utils"; +import { getGlobalFlags, shouldDisableAutostart } from "../cliConfig"; import { type Commands } from "../commands"; import { type CliManager } from "../core/cliManager"; import * as cliUtils from "../core/cliUtils"; @@ -27,7 +28,6 @@ import { type ServiceContainer } from "../core/container"; import { type ContextManager } from "../core/contextManager"; import { type PathResolver } from "../core/pathResolver"; import { featureSetForVersion, type FeatureSet } from "../featureSet"; -import { getGlobalFlags } from "../globalFlags"; import { Inbox } from "../inbox"; import { type Logger } from "../logging/logger"; import { @@ -669,7 +669,7 @@ export class Remote { const globalConfigs = this.globalConfigs(label); const proxyCommand = featureSet.wildcardSSH - ? `${escapeCommandArg(binaryPath)}${globalConfigs} ssh --stdio --usage-app=vscode --disable-autostart --network-info-dir ${escapeCommandArg(this.pathResolver.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h` + ? `${escapeCommandArg(binaryPath)}${globalConfigs} ssh --stdio --usage-app=vscode${this.disableAutostartConfig()} --network-info-dir ${escapeCommandArg(this.pathResolver.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h` : `${escapeCommandArg(binaryPath)}${globalConfigs} vscodessh --network-info-dir ${escapeCommandArg( this.pathResolver.getNetworkInfoPath(), )}${await this.formatLogArg(logDir)} --session-token-file ${escapeCommandArg(this.pathResolver.getSessionTokenPath(label))} --url-file ${escapeCommandArg( @@ -812,6 +812,13 @@ export class Remote { return [statusBarItem, agentWatcher, onChangeDisposable]; } + private disableAutostartConfig(): string { + const configs = vscode.workspace.getConfiguration(); + return shouldDisableAutostart(configs, process.platform) + ? " --disable-autostart" + : ""; + } + // closeRemote ends the current remote session. public async closeRemote() { await vscode.commands.executeCommand("workbench.action.remote.close"); diff --git a/test/unit/cliConfig.test.ts b/test/unit/cliConfig.test.ts new file mode 100644 index 00000000..a8a39bc6 --- /dev/null +++ b/test/unit/cliConfig.test.ts @@ -0,0 +1,132 @@ +import { it, expect, describe } from "vitest"; +import { type WorkspaceConfiguration } from "vscode"; + +import { getGlobalFlags, shouldDisableAutostart } from "@/cliConfig"; + +import { isWindows } from "../utils/platform"; + +describe("cliConfig", () => { + describe("getGlobalFlags", () => { + it("should return global-config and header args when no global flags configured", () => { + const config = { + get: () => undefined, + } as unknown as WorkspaceConfiguration; + + expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([ + "--global-config", + '"/config/dir"', + ]); + }); + + it("should return global flags from config with global-config appended", () => { + const config = { + get: (key: string) => + key === "coder.globalFlags" + ? ["--verbose", "--disable-direct-connections"] + : undefined, + } as unknown as WorkspaceConfiguration; + + expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([ + "--verbose", + "--disable-direct-connections", + "--global-config", + '"/config/dir"', + ]); + }); + + it("should not filter duplicate global-config flags, last takes precedence", () => { + const config = { + get: (key: string) => + key === "coder.globalFlags" + ? [ + "-v", + "--global-config /path/to/ignored", + "--disable-direct-connections", + ] + : undefined, + } as unknown as WorkspaceConfiguration; + + expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([ + "-v", + "--global-config /path/to/ignored", + "--disable-direct-connections", + "--global-config", + '"/config/dir"', + ]); + }); + + it("should not filter header-command flags, header args appended at end", () => { + const headerCommand = "echo test"; + const config = { + get: (key: string) => { + if (key === "coder.headerCommand") { + return headerCommand; + } + if (key === "coder.globalFlags") { + return ["-v", "--header-command custom", "--no-feature-warning"]; + } + return undefined; + }, + } as unknown as WorkspaceConfiguration; + + const result = getGlobalFlags(config, "/config/dir"); + expect(result).toStrictEqual([ + "-v", + "--header-command custom", // ignored by CLI + "--no-feature-warning", + "--global-config", + '"/config/dir"', + "--header-command", + quoteCommand(headerCommand), + ]); + }); + }); + + describe("shouldDisableAutostart", () => { + const mockConfig = (setting: string) => + ({ + get: (key: string) => + key === "coder.disableAutostart" ? setting : undefined, + }) as unknown as WorkspaceConfiguration; + + it("returns true when setting is 'always' regardless of platform", () => { + const config = mockConfig("always"); + expect(shouldDisableAutostart(config, "darwin")).toBe(true); + expect(shouldDisableAutostart(config, "linux")).toBe(true); + expect(shouldDisableAutostart(config, "win32")).toBe(true); + }); + + it("returns false when setting is 'never' regardless of platform", () => { + const config = mockConfig("never"); + expect(shouldDisableAutostart(config, "darwin")).toBe(false); + expect(shouldDisableAutostart(config, "linux")).toBe(false); + expect(shouldDisableAutostart(config, "win32")).toBe(false); + }); + + it("returns true when setting is 'auto' and platform is darwin", () => { + const config = mockConfig("auto"); + expect(shouldDisableAutostart(config, "darwin")).toBe(true); + }); + + it("returns false when setting is 'auto' and platform is not darwin", () => { + const config = mockConfig("auto"); + expect(shouldDisableAutostart(config, "linux")).toBe(false); + expect(shouldDisableAutostart(config, "win32")).toBe(false); + expect(shouldDisableAutostart(config, "freebsd")).toBe(false); + }); + + it("defaults to 'auto' when setting is not configured", () => { + const config = { + get: (_key: string, defaultValue: unknown) => defaultValue, + } as unknown as WorkspaceConfiguration; + expect(shouldDisableAutostart(config, "darwin")).toBe(true); + expect(shouldDisableAutostart(config, "linux")).toBe(false); + }); + }); +}); + +function quoteCommand(value: string): string { + // Used to escape environment variables in commands. See `getHeaderArgs` in src/headers.ts + const quote = isWindows() ? '"' : "'"; + return `${quote}${value}${quote}`; +} diff --git a/test/unit/globalFlags.test.ts b/test/unit/globalFlags.test.ts deleted file mode 100644 index 94c89dba..00000000 --- a/test/unit/globalFlags.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { it, expect, describe } from "vitest"; -import { type WorkspaceConfiguration } from "vscode"; - -import { getGlobalFlags } from "@/globalFlags"; - -import { isWindows } from "../utils/platform"; - -describe("Global flags suite", () => { - it("should return global-config and header args when no global flags configured", () => { - const config = { - get: () => undefined, - } as unknown as WorkspaceConfiguration; - - expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([ - "--global-config", - '"/config/dir"', - ]); - }); - - it("should return global flags from config with global-config appended", () => { - const config = { - get: (key: string) => - key === "coder.globalFlags" - ? ["--verbose", "--disable-direct-connections"] - : undefined, - } as unknown as WorkspaceConfiguration; - - expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([ - "--verbose", - "--disable-direct-connections", - "--global-config", - '"/config/dir"', - ]); - }); - - it("should not filter duplicate global-config flags, last takes precedence", () => { - const config = { - get: (key: string) => - key === "coder.globalFlags" - ? [ - "-v", - "--global-config /path/to/ignored", - "--disable-direct-connections", - ] - : undefined, - } as unknown as WorkspaceConfiguration; - - expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([ - "-v", - "--global-config /path/to/ignored", - "--disable-direct-connections", - "--global-config", - '"/config/dir"', - ]); - }); - - it("should not filter header-command flags, header args appended at end", () => { - const headerCommand = "echo test"; - const config = { - get: (key: string) => { - if (key === "coder.headerCommand") { - return headerCommand; - } - if (key === "coder.globalFlags") { - return ["-v", "--header-command custom", "--no-feature-warning"]; - } - return undefined; - }, - } as unknown as WorkspaceConfiguration; - - const result = getGlobalFlags(config, "/config/dir"); - expect(result).toStrictEqual([ - "-v", - "--header-command custom", // ignored by CLI - "--no-feature-warning", - "--global-config", - '"/config/dir"', - "--header-command", - quoteCommand(headerCommand), - ]); - }); -}); - -function quoteCommand(value: string): string { - // Used to escape environment variables in commands. See `getHeaderArgs` in src/headers.ts - const quote = isWindows() ? '"' : "'"; - return `${quote}${value}${quote}`; -} From 581d285bb9cdfebfc5159b86a386b1c6764d5f8d Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Mon, 8 Dec 2025 13:30:41 +0300 Subject: [PATCH 2/6] Add support to `coder.sshFlags` array for user configurable SSH options --- package.json | 9 ++- src/cliConfig.ts | 7 +++ src/remote/remote.ts | 115 ++++++++++++++++++++++++++---------- test/unit/cliConfig.test.ts | 32 +++++++++- 4 files changed, 131 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 7685dffb..db349499 100644 --- a/package.json +++ b/package.json @@ -130,11 +130,18 @@ ], "markdownEnumDescriptions": [ "Disables autostart on macOS only (recommended to avoid sleep/wake issues)", - "Disables on all platforms", + "Disables autostart on all platforms", "Keeps autostart enabled on all platforms" ], "default": "auto" }, + "coder.sshFlags": { + "markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote that the following flags are ignored because they are managed internally by the extension: `--network-info-dir`, `--log-dir`, `--ssh-host-prefix`. To manage the `--disable-autostart` flag, use the `coder.disableAutostart` setting.", + "type": "array", + "items": { + "type": "string" + } + }, "coder.globalFlags": { "markdownDescription": "Global flags to pass to every Coder CLI invocation. Enter each flag as a separate array item; values are passed verbatim and in order. Do **not** include the `coder` command itself. See the [CLI reference](https://coder.com/docs/reference/cli) for available global flags.\n\nNote that for `--header-command`, precedence is: `#coder.headerCommand#` setting, then `CODER_HEADER_COMMAND` environment variable, then the value specified here. The `--global-config` flag is explicitly ignored.", "type": "array", diff --git a/src/cliConfig.ts b/src/cliConfig.ts index 4d8c9000..c14a8782 100644 --- a/src/cliConfig.ts +++ b/src/cliConfig.ts @@ -38,3 +38,10 @@ export function shouldDisableAutostart( ); return setting === "always" || (setting === "auto" && platform === "darwin"); } + +/** + * Returns SSH flags for the `coder ssh` command from user configuration. + */ +export function getSshFlags(configs: WorkspaceConfiguration): string[] { + return configs.get("coder.sshFlags") || []; +} diff --git a/src/remote/remote.ts b/src/remote/remote.ts index 201d9fd3..896360ec 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -20,7 +20,11 @@ import { import { extractAgents } from "../api/api-helper"; import { CoderApi } from "../api/coderApi"; import { needToken } from "../api/utils"; -import { getGlobalFlags, shouldDisableAutostart } from "../cliConfig"; +import { + getGlobalFlags, + getSshFlags, + shouldDisableAutostart, +} from "../cliConfig"; import { type Commands } from "../commands"; import { type CliManager } from "../core/cliManager"; import * as cliUtils from "../core/cliUtils"; @@ -571,16 +575,85 @@ export class Remote { } /** - * Formats the --log-dir argument for the ProxyCommand after making sure it + * Builds the ProxyCommand for SSH connections to Coder workspaces. + * Uses `coder ssh` for modern deployments with wildcard support, + * or falls back to `coder vscodessh` for older deployments. + */ + private async buildProxyCommand( + binaryPath: string, + label: string, + hostPrefix: string, + logDir: string, + useWildcardSSH: boolean, + ): Promise { + const vscodeConfig = vscode.workspace.getConfiguration(); + + const escapedBinaryPath = escapeCommandArg(binaryPath); + const globalConfig = getGlobalFlags( + vscodeConfig, + this.pathResolver.getGlobalConfigDir(label), + ); + const logArgs = await this.getLogArgs(logDir); + + if (useWildcardSSH) { + // User SSH flags are included first; internally-managed flags + // are appended last so they take precedence. + const userSSHFlags = getSshFlags(vscodeConfig); + const disableAutostart = shouldDisableAutostart( + vscodeConfig, + process.platform, + ) + ? ["--disable-autostart"] + : []; + const internalFlags = [ + "--stdio", + "--usage-app=vscode", + "--network-info-dir", + escapeCommandArg(this.pathResolver.getNetworkInfoPath()), + ...logArgs, + ...disableAutostart, + "--ssh-host-prefix", + hostPrefix, + "%h", + ]; + + const allFlags = [...userSSHFlags, ...internalFlags]; + return `${escapedBinaryPath} ${globalConfig.join(" ")} ssh ${allFlags.join(" ")}`; + } else { + const networkInfoDir = escapeCommandArg( + this.pathResolver.getNetworkInfoPath(), + ); + const sessionTokenFile = escapeCommandArg( + this.pathResolver.getSessionTokenPath(label), + ); + const urlFile = escapeCommandArg(this.pathResolver.getUrlPath(label)); + + const sshFlags = [ + "--network-info-dir", + networkInfoDir, + ...logArgs, + "--session-token-file", + sessionTokenFile, + "--url-file", + urlFile, + "%h", + ]; + + return `${escapedBinaryPath} ${globalConfig.join(" ")} vscodessh ${sshFlags.join(" ")}`; + } + } + + /** + * Returns the --log-dir argument for the ProxyCommand after making sure it * has been created. */ - private async formatLogArg(logDir: string): Promise { + private async getLogArgs(logDir: string): Promise { if (!logDir) { - return ""; + return []; } await fs.mkdir(logDir, { recursive: true }); this.logger.info("SSH proxy diagnostics are being written to", logDir); - return ` --log-dir ${escapeCommandArg(logDir)} -v`; + return ["--log-dir", escapeCommandArg(logDir), "-v"]; } // updateSSHConfig updates the SSH configuration with a wildcard that handles @@ -666,15 +739,13 @@ export class Remote { ? `${AuthorityPrefix}.${label}--` : `${AuthorityPrefix}--`; - const globalConfigs = this.globalConfigs(label); - - const proxyCommand = featureSet.wildcardSSH - ? `${escapeCommandArg(binaryPath)}${globalConfigs} ssh --stdio --usage-app=vscode${this.disableAutostartConfig()} --network-info-dir ${escapeCommandArg(this.pathResolver.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h` - : `${escapeCommandArg(binaryPath)}${globalConfigs} vscodessh --network-info-dir ${escapeCommandArg( - this.pathResolver.getNetworkInfoPath(), - )}${await this.formatLogArg(logDir)} --session-token-file ${escapeCommandArg(this.pathResolver.getSessionTokenPath(label))} --url-file ${escapeCommandArg( - this.pathResolver.getUrlPath(label), - )} %h`; + const proxyCommand = await this.buildProxyCommand( + binaryPath, + label, + hostPrefix, + logDir, + featureSet.wildcardSSH, + ); const sshValues: SSHValues = { Host: hostPrefix + `*`, @@ -727,15 +798,6 @@ export class Remote { return sshConfig.getRaw(); } - private globalConfigs(label: string): string { - const vscodeConfig = vscode.workspace.getConfiguration(); - const args = getGlobalFlags( - vscodeConfig, - this.pathResolver.getGlobalConfigDir(label), - ); - return ` ${args.join(" ")}`; - } - private watchLogDirSetting( currentLogDir: string, featureSet: FeatureSet, @@ -812,13 +874,6 @@ export class Remote { return [statusBarItem, agentWatcher, onChangeDisposable]; } - private disableAutostartConfig(): string { - const configs = vscode.workspace.getConfiguration(); - return shouldDisableAutostart(configs, process.platform) - ? " --disable-autostart" - : ""; - } - // closeRemote ends the current remote session. public async closeRemote() { await vscode.commands.executeCommand("workbench.action.remote.close"); diff --git a/test/unit/cliConfig.test.ts b/test/unit/cliConfig.test.ts index a8a39bc6..98f6d031 100644 --- a/test/unit/cliConfig.test.ts +++ b/test/unit/cliConfig.test.ts @@ -1,7 +1,11 @@ import { it, expect, describe } from "vitest"; import { type WorkspaceConfiguration } from "vscode"; -import { getGlobalFlags, shouldDisableAutostart } from "@/cliConfig"; +import { + getGlobalFlags, + getSshFlags, + shouldDisableAutostart, +} from "@/cliConfig"; import { isWindows } from "../utils/platform"; @@ -123,6 +127,32 @@ describe("cliConfig", () => { expect(shouldDisableAutostart(config, "linux")).toBe(false); }); }); + + describe("getSshFlags", () => { + it("returns empty array when no SSH flags configured", () => { + const config = { + get: () => undefined, + } as unknown as WorkspaceConfiguration; + + expect(getSshFlags(config)).toStrictEqual([]); + }); + + it("returns SSH flags from config", () => { + const config = { + get: (key: string) => + key === "coder.sshFlags" + ? ["--disable-autostart", "--wait=yes", "--ssh-host-prefix=custom"] + : undefined, + } as unknown as WorkspaceConfiguration; + + expect(getSshFlags(config)).toStrictEqual([ + "--disable-autostart", + "--wait=yes", + // No filtering and returned as-is (even though it'll be overridden later) + "--ssh-host-prefix=custom", + ]); + }); + }); }); function quoteCommand(value: string): string { From 93f4d64c0add572aade9d6bbda549b741ff3b0e0 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 9 Dec 2025 11:49:24 +0300 Subject: [PATCH 3/6] Update description and make sure to read log dir file from `coder.sshFlags` --- package.json | 2 +- src/remote/remote.ts | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index db349499..85b81990 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "default": "auto" }, "coder.sshFlags": { - "markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote that the following flags are ignored because they are managed internally by the extension: `--network-info-dir`, `--log-dir`, `--ssh-host-prefix`. To manage the `--disable-autostart` flag, use the `coder.disableAutostart` setting.", + "markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote: `--network-info-dir` and `--ssh-host-prefix` are ignored (managed internally). `--disable-autostart` and `--log-dir`/`-l` can be specified here, but prefer using `#coder.disableAutostart#` and `#coder.proxyLogDirectory#` settings respectively.", "type": "array", "items": { "type": "string" diff --git a/src/remote/remote.ts b/src/remote/remote.ts index 896360ec..ed58e03e 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -558,20 +558,30 @@ export class Remote { } /** - * Return the --log-dir argument value for the ProxyCommand. It may be an + * Return the --log-dir argument value for the ProxyCommand. It may be an * empty string if the setting is not set or the cli does not support it. */ private getLogDir(featureSet: FeatureSet): string { if (!featureSet.proxyLogDirectory) { return ""; } - // If the proxyLogDirectory is not set in the extension settings we don't send one. - return expandPath( - String( - vscode.workspace.getConfiguration().get("coder.proxyLogDirectory") ?? - "", - ).trim(), + + const vscodeConfig = vscode.workspace.getConfiguration(); + const proxyLogDir = vscodeConfig + .get("coder.proxyLogDirectory") + ?.trim(); + if (proxyLogDir) { + return expandPath(proxyLogDir); + } + + const sshFlags = getSshFlags(vscodeConfig); + const logDirFlagIndex = sshFlags.findIndex( + (option) => option === "-l" || option === "--log-dir", ); + if (logDirFlagIndex !== -1 && logDirFlagIndex + 1 < sshFlags.length) { + return expandPath(sshFlags[logDirFlagIndex + 1].trim()); + } + return ""; } /** @@ -605,6 +615,7 @@ export class Remote { ) ? ["--disable-autostart"] : []; + // Make sure to update the `coder.sshFlags` description if we add more internal flags here! const internalFlags = [ "--stdio", "--usage-app=vscode", From 0a1e51f23b6660ad460a75f98b51e91619959d89 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 10 Dec 2025 12:05:35 +0300 Subject: [PATCH 4/6] Remove the `coder.disableAutostart` setting and enhance documentation --- CHANGELOG.md | 1 + package.json | 22 ++++------------- src/cliConfig.ts | 19 --------------- src/remote/remote.ts | 37 +++++++--------------------- test/unit/cliConfig.test.ts | 48 +------------------------------------ 5 files changed, 16 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 425ed11a..1166d82c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Support for paths that begin with a tilde (`~`). +- Support for `coder ssh` flag configurations through the `coder.sshFlags` setting. ### Fixed diff --git a/package.json b/package.json index 85b81990..b06f2a76 100644 --- a/package.json +++ b/package.json @@ -120,27 +120,15 @@ "type": "boolean", "default": false }, - "coder.disableAutostart": { - "markdownDescription": "Disable starting the workspace automatically when connecting via SSH.", - "type": "string", - "enum": [ - "auto", - "always", - "never" - ], - "markdownEnumDescriptions": [ - "Disables autostart on macOS only (recommended to avoid sleep/wake issues)", - "Disables autostart on all platforms", - "Keeps autostart enabled on all platforms" - ], - "default": "auto" - }, "coder.sshFlags": { - "markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote: `--network-info-dir` and `--ssh-host-prefix` are ignored (managed internally). `--disable-autostart` and `--log-dir`/`-l` can be specified here, but prefer using `#coder.disableAutostart#` and `#coder.proxyLogDirectory#` settings respectively.", + "markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote: `--network-info-dir` and `--ssh-host-prefix` are ignored (managed internally). Prefer `#coder.proxyLogDirectory#` over `--log-dir`/`-l` for full functionality.", "type": "array", "items": { "type": "string" - } + }, + "default": [ + "--disable-autostart" + ] }, "coder.globalFlags": { "markdownDescription": "Global flags to pass to every Coder CLI invocation. Enter each flag as a separate array item; values are passed verbatim and in order. Do **not** include the `coder` command itself. See the [CLI reference](https://coder.com/docs/reference/cli) for available global flags.\n\nNote that for `--header-command`, precedence is: `#coder.headerCommand#` setting, then `CODER_HEADER_COMMAND` environment variable, then the value specified here. The `--global-config` flag is explicitly ignored.", diff --git a/src/cliConfig.ts b/src/cliConfig.ts index c14a8782..ed02627e 100644 --- a/src/cliConfig.ts +++ b/src/cliConfig.ts @@ -20,25 +20,6 @@ export function getGlobalFlags( ]; } -type DisableAutostartSetting = "auto" | "always" | "never"; - -/** - * Determines whether autostart should be disabled based on the setting and platform. - * - "always": disable on all platforms - * - "never": never disable - * - "auto": disable only on macOS (due to sleep/wake issues) - */ -export function shouldDisableAutostart( - configs: WorkspaceConfiguration, - platform: NodeJS.Platform, -): boolean { - const setting = configs.get( - "coder.disableAutostart", - "auto", - ); - return setting === "always" || (setting === "auto" && platform === "darwin"); -} - /** * Returns SSH flags for the `coder ssh` command from user configuration. */ diff --git a/src/remote/remote.ts b/src/remote/remote.ts index ed58e03e..e721aed4 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -20,11 +20,7 @@ import { import { extractAgents } from "../api/api-helper"; import { CoderApi } from "../api/coderApi"; import { needToken } from "../api/utils"; -import { - getGlobalFlags, - getSshFlags, - shouldDisableAutostart, -} from "../cliConfig"; +import { getGlobalFlags, getSshFlags } from "../cliConfig"; import { type Commands } from "../commands"; import { type CliManager } from "../core/cliManager"; import * as cliUtils from "../core/cliUtils"; @@ -560,28 +556,20 @@ export class Remote { /** * Return the --log-dir argument value for the ProxyCommand. It may be an * empty string if the setting is not set or the cli does not support it. + * + * Value defined in the "coder.sshFlags" setting is not considered. */ private getLogDir(featureSet: FeatureSet): string { if (!featureSet.proxyLogDirectory) { return ""; } - - const vscodeConfig = vscode.workspace.getConfiguration(); - const proxyLogDir = vscodeConfig - .get("coder.proxyLogDirectory") - ?.trim(); - if (proxyLogDir) { - return expandPath(proxyLogDir); - } - - const sshFlags = getSshFlags(vscodeConfig); - const logDirFlagIndex = sshFlags.findIndex( - (option) => option === "-l" || option === "--log-dir", + // If the proxyLogDirectory is not set in the extension settings we don't send one. + return expandPath( + String( + vscode.workspace.getConfiguration().get("coder.proxyLogDirectory") ?? + "", + ).trim(), ); - if (logDirFlagIndex !== -1 && logDirFlagIndex + 1 < sshFlags.length) { - return expandPath(sshFlags[logDirFlagIndex + 1].trim()); - } - return ""; } /** @@ -609,12 +597,6 @@ export class Remote { // User SSH flags are included first; internally-managed flags // are appended last so they take precedence. const userSSHFlags = getSshFlags(vscodeConfig); - const disableAutostart = shouldDisableAutostart( - vscodeConfig, - process.platform, - ) - ? ["--disable-autostart"] - : []; // Make sure to update the `coder.sshFlags` description if we add more internal flags here! const internalFlags = [ "--stdio", @@ -622,7 +604,6 @@ export class Remote { "--network-info-dir", escapeCommandArg(this.pathResolver.getNetworkInfoPath()), ...logArgs, - ...disableAutostart, "--ssh-host-prefix", hostPrefix, "%h", diff --git a/test/unit/cliConfig.test.ts b/test/unit/cliConfig.test.ts index 98f6d031..9fed2f60 100644 --- a/test/unit/cliConfig.test.ts +++ b/test/unit/cliConfig.test.ts @@ -1,11 +1,7 @@ import { it, expect, describe } from "vitest"; import { type WorkspaceConfiguration } from "vscode"; -import { - getGlobalFlags, - getSshFlags, - shouldDisableAutostart, -} from "@/cliConfig"; +import { getGlobalFlags, getSshFlags } from "@/cliConfig"; import { isWindows } from "../utils/platform"; @@ -86,48 +82,6 @@ describe("cliConfig", () => { }); }); - describe("shouldDisableAutostart", () => { - const mockConfig = (setting: string) => - ({ - get: (key: string) => - key === "coder.disableAutostart" ? setting : undefined, - }) as unknown as WorkspaceConfiguration; - - it("returns true when setting is 'always' regardless of platform", () => { - const config = mockConfig("always"); - expect(shouldDisableAutostart(config, "darwin")).toBe(true); - expect(shouldDisableAutostart(config, "linux")).toBe(true); - expect(shouldDisableAutostart(config, "win32")).toBe(true); - }); - - it("returns false when setting is 'never' regardless of platform", () => { - const config = mockConfig("never"); - expect(shouldDisableAutostart(config, "darwin")).toBe(false); - expect(shouldDisableAutostart(config, "linux")).toBe(false); - expect(shouldDisableAutostart(config, "win32")).toBe(false); - }); - - it("returns true when setting is 'auto' and platform is darwin", () => { - const config = mockConfig("auto"); - expect(shouldDisableAutostart(config, "darwin")).toBe(true); - }); - - it("returns false when setting is 'auto' and platform is not darwin", () => { - const config = mockConfig("auto"); - expect(shouldDisableAutostart(config, "linux")).toBe(false); - expect(shouldDisableAutostart(config, "win32")).toBe(false); - expect(shouldDisableAutostart(config, "freebsd")).toBe(false); - }); - - it("defaults to 'auto' when setting is not configured", () => { - const config = { - get: (_key: string, defaultValue: unknown) => defaultValue, - } as unknown as WorkspaceConfiguration; - expect(shouldDisableAutostart(config, "darwin")).toBe(true); - expect(shouldDisableAutostart(config, "linux")).toBe(false); - }); - }); - describe("getSshFlags", () => { it("returns empty array when no SSH flags configured", () => { const config = { From ecc3f57a2fc25505ba56c852595641a4373254e6 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 10 Dec 2025 12:31:27 +0300 Subject: [PATCH 5/6] Fix setting the default flags --- src/cliConfig.ts | 3 ++- src/remote/remote.ts | 4 ++-- test/unit/cliConfig.test.ts | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/cliConfig.ts b/src/cliConfig.ts index ed02627e..0ae0080f 100644 --- a/src/cliConfig.ts +++ b/src/cliConfig.ts @@ -24,5 +24,6 @@ export function getGlobalFlags( * Returns SSH flags for the `coder ssh` command from user configuration. */ export function getSshFlags(configs: WorkspaceConfiguration): string[] { - return configs.get("coder.sshFlags") || []; + // Make sure to match this default with the one in the package.json + return configs.get("coder.sshFlags", ["--disable-autostart"]); } diff --git a/src/remote/remote.ts b/src/remote/remote.ts index e721aed4..d0d30f50 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -596,7 +596,7 @@ export class Remote { if (useWildcardSSH) { // User SSH flags are included first; internally-managed flags // are appended last so they take precedence. - const userSSHFlags = getSshFlags(vscodeConfig); + const userSshFlags = getSshFlags(vscodeConfig); // Make sure to update the `coder.sshFlags` description if we add more internal flags here! const internalFlags = [ "--stdio", @@ -609,7 +609,7 @@ export class Remote { "%h", ]; - const allFlags = [...userSSHFlags, ...internalFlags]; + const allFlags = [...userSshFlags, ...internalFlags]; return `${escapedBinaryPath} ${globalConfig.join(" ")} ssh ${allFlags.join(" ")}`; } else { const networkInfoDir = escapeCommandArg( diff --git a/test/unit/cliConfig.test.ts b/test/unit/cliConfig.test.ts index 9fed2f60..d350dcbd 100644 --- a/test/unit/cliConfig.test.ts +++ b/test/unit/cliConfig.test.ts @@ -83,12 +83,12 @@ describe("cliConfig", () => { }); describe("getSshFlags", () => { - it("returns empty array when no SSH flags configured", () => { + it("returns default flags when no SSH flags configured", () => { const config = { - get: () => undefined, + get: (_key: string, defaultValue: unknown) => defaultValue, } as unknown as WorkspaceConfiguration; - expect(getSshFlags(config)).toStrictEqual([]); + expect(getSshFlags(config)).toStrictEqual(["--disable-autostart"]); }); it("returns SSH flags from config", () => { From bef438b1549bb76455978eb08ad933bd5bbea59f Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 10 Dec 2025 12:58:17 +0300 Subject: [PATCH 6/6] Watch variables in remote and prompt the user to restart the window --- src/remote/remote.ts | 51 +++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/remote/remote.ts b/src/remote/remote.ts index d0d30f50..27a0477e 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -501,8 +501,6 @@ export class Remote { sshMonitor.onLogFilePathChange((newPath) => { this.commands.workspaceLogPath = newPath; }), - // Watch for logDir configuration changes - this.watchLogDirSetting(logDir, featureSet), // Register the label formatter again because SSH overrides it! vscode.extensions.onDidChange(() => { // Dispose previous label formatter @@ -516,6 +514,18 @@ export class Remote { }), ...(await this.createAgentMetadataStatusBar(agent, workspaceClient)), ); + + const settingsToWatch = [ + { setting: "coder.globalFlags", title: "Global flags" }, + { setting: "coder.sshFlags", title: "SSH flags" }, + ]; + if (featureSet.proxyLogDirectory) { + settingsToWatch.push({ + setting: "coder.proxyLogDirectory", + title: "Proxy log directory", + }); + } + disposables.push(this.watchSettings(settingsToWatch)); } catch (ex) { // Whatever error happens, make sure we clean up the disposables in case of failure disposables.forEach((d) => d.dispose()); @@ -790,29 +800,26 @@ export class Remote { return sshConfig.getRaw(); } - private watchLogDirSetting( - currentLogDir: string, - featureSet: FeatureSet, + private watchSettings( + settings: Array<{ setting: string; title: string }>, ): vscode.Disposable { return vscode.workspace.onDidChangeConfiguration((e) => { - if (!e.affectsConfiguration("coder.proxyLogDirectory")) { - return; - } - const newLogDir = this.getLogDir(featureSet); - if (newLogDir === currentLogDir) { - return; + for (const { setting, title } of settings) { + if (!e.affectsConfiguration(setting)) { + continue; + } + vscode.window + .showInformationMessage( + `${title} setting changed. Reload window to apply.`, + "Reload", + ) + .then((action) => { + if (action === "Reload") { + vscode.commands.executeCommand("workbench.action.reloadWindow"); + } + }); + break; } - - vscode.window - .showInformationMessage( - "Log directory configuration changed. Reload window to apply.", - "Reload", - ) - .then((action) => { - if (action === "Reload") { - vscode.commands.executeCommand("workbench.action.reloadWindow"); - } - }); }); }