@@ -11,12 +11,12 @@ import { type CoderApi } from "./api/coderApi";
1111import { getGlobalFlags } from "./cliConfig" ;
1212import { type CliManager } from "./core/cliManager" ;
1313import { type ServiceContainer } from "./core/container" ;
14- import { type ContextManager } from "./core/contextManager" ;
1514import { type MementoManager } from "./core/mementoManager" ;
1615import { type PathResolver } from "./core/pathResolver" ;
1716import { type SecretsManager } from "./core/secretsManager" ;
1817import { type DeploymentManager } from "./deployment/deploymentManager" ;
1918import { CertificateError } from "./error/certificateError" ;
19+ import { toError } from "./error/errorUtils" ;
2020import { type Logger } from "./logging/logger" ;
2121import { type LoginCoordinator } from "./login/loginCoordinator" ;
2222import { maybeAskAgent , maybeAskUrl } from "./promptUtils" ;
@@ -34,7 +34,6 @@ export class Commands {
3434 private readonly mementoManager : MementoManager ;
3535 private readonly secretsManager : SecretsManager ;
3636 private readonly cliManager : CliManager ;
37- private readonly contextManager : ContextManager ;
3837 private readonly loginCoordinator : LoginCoordinator ;
3938
4039 // These will only be populated when actively connected to a workspace and are
@@ -58,7 +57,6 @@ export class Commands {
5857 this . mementoManager = serviceContainer . getMementoManager ( ) ;
5958 this . secretsManager = serviceContainer . getSecretsManager ( ) ;
6059 this . cliManager = serviceContainer . getCliManager ( ) ;
61- this . contextManager = serviceContainer . getContextManager ( ) ;
6260 this . loginCoordinator = serviceContainer . getLoginCoordinator ( ) ;
6361 }
6462
@@ -74,9 +72,8 @@ export class Commands {
7472 }
7573
7674 /**
77- * Log into the provided deployment. If the deployment URL is not specified,
78- * ask for it first with a menu showing recent URLs along with the default URL
79- * and CODER_URL, if those are set.
75+ * Log into a deployment. If already authenticated, this is a no-op.
76+ * If no URL is provided, shows a menu of recent URLs plus defaults.
8077 */
8178 public async login ( args ?: {
8279 url ?: string ;
@@ -85,6 +82,13 @@ export class Commands {
8582 if ( this . deploymentManager . isAuthenticated ( ) ) {
8683 return ;
8784 }
85+ await this . performLogin ( args ) ;
86+ }
87+
88+ private async performLogin ( args ?: {
89+ url ?: string ;
90+ autoLogin ?: boolean ;
91+ } ) : Promise < void > {
8892 this . logger . debug ( "Logging in" ) ;
8993
9094 const currentDeployment = await this . secretsManager . getCurrentDeployment ( ) ;
@@ -197,7 +201,7 @@ export class Commands {
197201 }
198202
199203 /**
200- * Log out from the currently logged-in deployment .
204+ * Log out and clear stored credentials, requiring re-authentication on next login .
201205 */
202206 public async logout ( ) : Promise < void > {
203207 if ( ! this . deploymentManager . isAuthenticated ( ) ) {
@@ -206,8 +210,15 @@ export class Commands {
206210
207211 this . logger . debug ( "Logging out" ) ;
208212
213+ const safeHostname =
214+ this . deploymentManager . getCurrentDeployment ( ) ?. safeHostname ;
215+
209216 await this . deploymentManager . clearDeployment ( ) ;
210217
218+ if ( safeHostname ) {
219+ await this . secretsManager . clearAllAuthData ( safeHostname ) ;
220+ }
221+
211222 vscode . window
212223 . showInformationMessage ( "You've been logged out of Coder!" , "Login" )
213224 . then ( ( action ) => {
@@ -221,6 +232,95 @@ export class Commands {
221232 this . logger . debug ( "Logout complete" ) ;
222233 }
223234
235+ /**
236+ * Switch to a different deployment without clearing credentials.
237+ * If login fails or user cancels, stays on current deployment.
238+ */
239+ public async switchDeployment ( ) : Promise < void > {
240+ this . logger . debug ( "Switching deployment" ) ;
241+ await this . performLogin ( ) ;
242+ }
243+
244+ /**
245+ * Manage stored credentials for all deployments.
246+ * Shows a list of deployments with options to remove individual or all credentials.
247+ */
248+ public async manageCredentials ( ) : Promise < void > {
249+ try {
250+ const hostnames = await this . secretsManager . getKnownSafeHostnames ( ) ;
251+ if ( hostnames . length === 0 ) {
252+ vscode . window . showInformationMessage ( "No stored credentials." ) ;
253+ return ;
254+ }
255+
256+ const items : Array < {
257+ label : string ;
258+ description : string ;
259+ hostnames : string [ ] ;
260+ } > = hostnames . map ( ( hostname ) => ( {
261+ label : `$(key) ${ hostname } ` ,
262+ description : "Remove stored credentials" ,
263+ hostnames : [ hostname ] ,
264+ } ) ) ;
265+
266+ // Only show "Remove All" when there are multiple deployments
267+ if ( hostnames . length > 1 ) {
268+ items . push ( {
269+ label : "$(trash) Remove All" ,
270+ description : `Remove credentials for all ${ hostnames . length } deployments` ,
271+ hostnames,
272+ } ) ;
273+ }
274+
275+ const selected = await vscode . window . showQuickPick ( items , {
276+ title : "Manage Stored Credentials" ,
277+ placeHolder : "Select a deployment to remove" ,
278+ } ) ;
279+
280+ if ( ! selected ) {
281+ return ;
282+ }
283+
284+ if ( selected . hostnames . length === 1 ) {
285+ const selectedHostname = selected . hostnames [ 0 ] ;
286+ await this . secretsManager . clearAllAuthData ( selectedHostname ) ;
287+ this . logger . info ( "Removed credentials for" , selectedHostname ) ;
288+ vscode . window . showInformationMessage (
289+ `Removed credentials for ${ selectedHostname } ` ,
290+ ) ;
291+ } else {
292+ const confirm = await vscodeProposed . window . showWarningMessage (
293+ `Remove ${ selected . hostnames . length } Credentials` ,
294+ {
295+ useCustom : true ,
296+ modal : true ,
297+ detail : `This will remove credentials for: ${ selected . hostnames . join ( ", " ) } \n\nYou'll need to log in again to access them.` ,
298+ } ,
299+ "Remove All" ,
300+ ) ;
301+ if ( confirm === "Remove All" ) {
302+ await Promise . all (
303+ selected . hostnames . map ( ( h ) =>
304+ this . secretsManager . clearAllAuthData ( h ) ,
305+ ) ,
306+ ) ;
307+ this . logger . info (
308+ "Removed credentials for all deployments:" ,
309+ selected . hostnames . join ( ", " ) ,
310+ ) ;
311+ vscode . window . showInformationMessage (
312+ "Removed credentials for all deployments" ,
313+ ) ;
314+ }
315+ }
316+ } catch ( error : unknown ) {
317+ this . logger . error ( "Failed to manage stored credentials" , error ) ;
318+ vscode . window . showErrorMessage (
319+ `Failed to manage stored credentials: ${ toError ( error ) . message } ` ,
320+ ) ;
321+ }
322+ }
323+
224324 /**
225325 * Create a new workspace for the currently logged-in deployment.
226326 *
0 commit comments