From b9da49b2d92c487fb7c9d02b6f023c9570782e6d Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Fri, 30 Jan 2026 13:01:52 +0300 Subject: [PATCH 1/3] Fix logout to clear credentials and add switch deployment command (#760) - Logout now clears stored credentials via secretsManager.clearAllAuthData() - Add "Switch Deployment" command to change deployments without clearing credentials --- package.json | 14 ++++++++++++++ src/commands.ts | 30 ++++++++++++++++++++++++++---- src/extension.ts | 4 ++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 818d42d3..b2442a6d 100644 --- a/package.json +++ b/package.json @@ -217,6 +217,12 @@ "category": "Coder", "icon": "$(sign-out)" }, + { + "command": "coder.switchDeployment", + "title": "Switch Deployment", + "category": "Coder", + "icon": "$(arrow-swap)" + }, { "command": "coder.open", "title": "Open Workspace", @@ -297,6 +303,10 @@ "command": "coder.logout", "when": "coder.authenticated" }, + { + "command": "coder.switchDeployment", + "when": "coder.authenticated" + }, { "command": "coder.createWorkspace", "when": "coder.authenticated" @@ -351,6 +361,10 @@ "command": "coder.logout", "when": "coder.authenticated && view == myWorkspaces" }, + { + "command": "coder.switchDeployment", + "when": "coder.authenticated && view == myWorkspaces" + }, { "command": "coder.login", "when": "!coder.authenticated && view == myWorkspaces" diff --git a/src/commands.ts b/src/commands.ts index 23b49b19..2baeece7 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -74,9 +74,8 @@ export class Commands { } /** - * Log into the provided deployment. If the deployment URL is not specified, - * ask for it first with a menu showing recent URLs along with the default URL - * and CODER_URL, if those are set. + * Log into a deployment. If already authenticated, this is a no-op. + * If no URL is provided, shows a menu of recent URLs plus defaults. */ public async login(args?: { url?: string; @@ -85,6 +84,13 @@ export class Commands { if (this.deploymentManager.isAuthenticated()) { return; } + await this.performLogin(args); + } + + private async performLogin(args?: { + url?: string; + autoLogin?: boolean; + }): Promise { this.logger.debug("Logging in"); const currentDeployment = await this.secretsManager.getCurrentDeployment(); @@ -197,7 +203,7 @@ export class Commands { } /** - * Log out from the currently logged-in deployment. + * Log out and clear stored credentials, requiring re-authentication on next login. */ public async logout(): Promise { if (!this.deploymentManager.isAuthenticated()) { @@ -206,8 +212,15 @@ export class Commands { this.logger.debug("Logging out"); + const safeHostname = + this.deploymentManager.getCurrentDeployment()?.safeHostname; + await this.deploymentManager.clearDeployment(); + if (safeHostname) { + await this.secretsManager.clearAllAuthData(safeHostname); + } + vscode.window .showInformationMessage("You've been logged out of Coder!", "Login") .then((action) => { @@ -221,6 +234,15 @@ export class Commands { this.logger.debug("Logout complete"); } + /** + * Switch to a different deployment without clearing credentials. + * If login fails or user cancels, stays on current deployment. + */ + public async switchDeployment(): Promise { + this.logger.debug("Switching deployment"); + await this.performLogin(); + } + /** * Create a new workspace for the currently logged-in deployment. * diff --git a/src/extension.ts b/src/extension.ts index 10902b3c..5019aedd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -197,6 +197,10 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { "coder.logout", commands.logout.bind(commands), ), + vscode.commands.registerCommand( + "coder.switchDeployment", + commands.switchDeployment.bind(commands), + ), vscode.commands.registerCommand("coder.open", commands.open.bind(commands)), vscode.commands.registerCommand( "coder.openDevContainer", From 065bc770516118587b0db21eb325edd7ee59e5a6 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Fri, 30 Jan 2026 13:38:45 +0300 Subject: [PATCH 2/3] Add manage credentials command Replace debug-only coder.debug.listDeployments with user-facing coder.manageCredentials command. Includes "Remove All" option with confirmation dialog when multiple deployments are stored. --- package.json | 9 +++--- src/commands.ts | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ src/extension.ts | 40 ++------------------------- 3 files changed, 79 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index b2442a6d..18ff08bb 100644 --- a/package.json +++ b/package.json @@ -288,9 +288,9 @@ "icon": "$(search)" }, { - "command": "coder.debug.listDeployments", - "title": "List Stored Deployments", - "category": "Coder Debug" + "command": "coder.manageCredentials", + "title": "Manage Stored Credentials", + "category": "Coder" } ], "menus": { @@ -352,8 +352,7 @@ "when": "false" }, { - "command": "coder.debug.listDeployments", - "when": "coder.devMode" + "command": "coder.manageCredentials" } ], "view/title": [ diff --git a/src/commands.ts b/src/commands.ts index 2baeece7..9a99ff5d 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -243,6 +243,78 @@ export class Commands { await this.performLogin(); } + /** + * Manage stored credentials for all deployments. + * Shows a list of deployments with options to remove individual or all credentials. + */ + public async manageCredentials(): Promise { + try { + const hostnames = await this.secretsManager.getKnownSafeHostnames(); + if (hostnames.length === 0) { + vscode.window.showInformationMessage("No stored credentials."); + return; + } + + const items: Array<{ + label: string; + description: string; + hostname: string | undefined; + }> = hostnames.map((hostname) => ({ + label: `$(key) ${hostname}`, + description: "Remove stored credentials", + hostname, + })); + + // Only show "Remove All" when there are multiple deployments + if (hostnames.length > 1) { + items.push({ + label: "$(trash) Remove All", + description: `Remove credentials for all ${hostnames.length} deployments`, + hostname: undefined, + }); + } + + const selected = await vscode.window.showQuickPick(items, { + title: "Manage Stored Credentials", + placeHolder: "Select a deployment to remove", + }); + + if (!selected) { + return; + } + + if (selected.hostname) { + await this.secretsManager.clearAllAuthData(selected.hostname); + vscode.window.showInformationMessage( + `Removed credentials for ${selected.hostname}`, + ); + } else { + const confirm = await vscodeProposed.window.showWarningMessage( + `Remove ${hostnames.length} Credentials`, + { + useCustom: true, + modal: true, + detail: `This will remove credentials for: ${hostnames.join(", ")}\n\nYou'll need to log in again to access them.`, + }, + "Remove All", + ); + if (confirm === "Remove All") { + await Promise.all( + hostnames.map((h) => this.secretsManager.clearAllAuthData(h)), + ); + vscode.window.showInformationMessage( + "Removed credentials for all deployments", + ); + } + } + } catch (error: unknown) { + this.logger.error("Failed to manage stored credentials", error); + vscode.window.showErrorMessage( + "Failed to manage stored credentials. Storage may be corrupted.", + ); + } + } + /** * Create a new workspace for the currently logged-in deployment. * diff --git a/src/extension.ts b/src/extension.ts index 5019aedd..8702ea52 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -244,8 +244,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { vscode.commands.registerCommand("coder.searchAllWorkspaces", async () => showTreeViewSearch(ALL_WORKSPACES_TREE_ID), ), - vscode.commands.registerCommand("coder.debug.listDeployments", () => - listStoredDeployments(serviceContainer), + vscode.commands.registerCommand( + "coder.manageCredentials", + commands.manageCredentials.bind(commands), ), ); @@ -387,38 +388,3 @@ async function showTreeViewSearch(id: string): Promise { await vscode.commands.executeCommand(`${id}.focus`); await vscode.commands.executeCommand("list.find"); } - -async function listStoredDeployments( - serviceContainer: ServiceContainer, -): Promise { - const secretsManager = serviceContainer.getSecretsManager(); - const output = serviceContainer.getLogger(); - - try { - const hostnames = await secretsManager.getKnownSafeHostnames(); - if (hostnames.length === 0) { - vscode.window.showInformationMessage("No deployments stored."); - return; - } - - const selected = await vscode.window.showQuickPick( - hostnames.map((hostname) => ({ - label: hostname, - description: "Click to forget", - })), - { placeHolder: "Select a deployment to forget" }, - ); - - if (selected) { - await secretsManager.clearAllAuthData(selected.label); - vscode.window.showInformationMessage( - `Cleared auth data for ${selected.label}`, - ); - } - } catch (error: unknown) { - output.error("Failed to list stored deployments", error); - vscode.window.showErrorMessage( - "Failed to list stored deployments. Storage may be corrupted.", - ); - } -} From 362b397c3647258389e049a79317ddbbbd5fb728 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Fri, 30 Jan 2026 19:13:29 +0300 Subject: [PATCH 3/3] Review comments --- package.json | 2 +- src/commands.ts | 32 +++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 18ff08bb..e27320d9 100644 --- a/package.json +++ b/package.json @@ -289,7 +289,7 @@ }, { "command": "coder.manageCredentials", - "title": "Manage Stored Credentials", + "title": "Manage Credentials", "category": "Coder" } ], diff --git a/src/commands.ts b/src/commands.ts index 9a99ff5d..738695e6 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -11,12 +11,12 @@ import { type CoderApi } from "./api/coderApi"; import { getGlobalFlags } from "./cliConfig"; import { type CliManager } from "./core/cliManager"; import { type ServiceContainer } from "./core/container"; -import { type ContextManager } from "./core/contextManager"; import { type MementoManager } from "./core/mementoManager"; import { type PathResolver } from "./core/pathResolver"; import { type SecretsManager } from "./core/secretsManager"; import { type DeploymentManager } from "./deployment/deploymentManager"; import { CertificateError } from "./error/certificateError"; +import { toError } from "./error/errorUtils"; import { type Logger } from "./logging/logger"; import { type LoginCoordinator } from "./login/loginCoordinator"; import { maybeAskAgent, maybeAskUrl } from "./promptUtils"; @@ -34,7 +34,6 @@ export class Commands { private readonly mementoManager: MementoManager; private readonly secretsManager: SecretsManager; private readonly cliManager: CliManager; - private readonly contextManager: ContextManager; private readonly loginCoordinator: LoginCoordinator; // These will only be populated when actively connected to a workspace and are @@ -58,7 +57,6 @@ export class Commands { this.mementoManager = serviceContainer.getMementoManager(); this.secretsManager = serviceContainer.getSecretsManager(); this.cliManager = serviceContainer.getCliManager(); - this.contextManager = serviceContainer.getContextManager(); this.loginCoordinator = serviceContainer.getLoginCoordinator(); } @@ -258,11 +256,11 @@ export class Commands { const items: Array<{ label: string; description: string; - hostname: string | undefined; + hostnames: string[]; }> = hostnames.map((hostname) => ({ label: `$(key) ${hostname}`, description: "Remove stored credentials", - hostname, + hostnames: [hostname], })); // Only show "Remove All" when there are multiple deployments @@ -270,7 +268,7 @@ export class Commands { items.push({ label: "$(trash) Remove All", description: `Remove credentials for all ${hostnames.length} deployments`, - hostname: undefined, + hostnames, }); } @@ -283,24 +281,32 @@ export class Commands { return; } - if (selected.hostname) { - await this.secretsManager.clearAllAuthData(selected.hostname); + if (selected.hostnames.length === 1) { + const selectedHostname = selected.hostnames[0]; + await this.secretsManager.clearAllAuthData(selectedHostname); + this.logger.info("Removed credentials for", selectedHostname); vscode.window.showInformationMessage( - `Removed credentials for ${selected.hostname}`, + `Removed credentials for ${selectedHostname}`, ); } else { const confirm = await vscodeProposed.window.showWarningMessage( - `Remove ${hostnames.length} Credentials`, + `Remove ${selected.hostnames.length} Credentials`, { useCustom: true, modal: true, - detail: `This will remove credentials for: ${hostnames.join(", ")}\n\nYou'll need to log in again to access them.`, + detail: `This will remove credentials for: ${selected.hostnames.join(", ")}\n\nYou'll need to log in again to access them.`, }, "Remove All", ); if (confirm === "Remove All") { await Promise.all( - hostnames.map((h) => this.secretsManager.clearAllAuthData(h)), + selected.hostnames.map((h) => + this.secretsManager.clearAllAuthData(h), + ), + ); + this.logger.info( + "Removed credentials for all deployments:", + selected.hostnames.join(", "), ); vscode.window.showInformationMessage( "Removed credentials for all deployments", @@ -310,7 +316,7 @@ export class Commands { } catch (error: unknown) { this.logger.error("Failed to manage stored credentials", error); vscode.window.showErrorMessage( - "Failed to manage stored credentials. Storage may be corrupted.", + `Failed to manage stored credentials: ${toError(error).message}`, ); } }