33 type WorkspaceAgent ,
44} from "coder/site/src/api/typesGenerated" ;
55import * as fs from "node:fs/promises" ;
6+ import * as os from "node:os" ;
67import * as path from "node:path" ;
78import * as semver from "semver" ;
89import * as vscode from "vscode" ;
@@ -22,7 +23,7 @@ import { type SecretsManager } from "./core/secretsManager";
2223import { type DeploymentManager } from "./deployment/deploymentManager" ;
2324import { CertificateError } from "./error/certificateError" ;
2425import { toError } from "./error/errorUtils" ;
25- import { featureSetForVersion } from "./featureSet" ;
26+ import { type FeatureSet , featureSetForVersion } from "./featureSet" ;
2627import { type Logger } from "./logging/logger" ;
2728import { type LoginCoordinator } from "./login/loginCoordinator" ;
2829import { withCancellableProgress , withProgress } from "./progress" ;
@@ -196,15 +197,17 @@ export class Commands {
196197 const trimmedDuration = duration . trim ( ) ;
197198
198199 const result = await withCancellableProgress (
199- async ( { signal } ) => {
200+ async ( { signal, progress } ) => {
201+ progress . report ( { message : "Resolving CLI..." } ) ;
200202 const env = await this . resolveCliEnv ( client ) ;
203+ progress . report ( { message : "Running..." } ) ;
201204 return cliExec . speedtest ( env , workspaceId , trimmedDuration , signal ) ;
202205 } ,
203206 {
204207 location : vscode . ProgressLocation . Notification ,
205208 title : trimmedDuration
206- ? `Running speed test (${ trimmedDuration } )... `
207- : "Running speed test..." ,
209+ ? `Speed test for ${ workspaceId } (${ trimmedDuration } )`
210+ : `Speed test for ${ workspaceId } ` ,
208211 cancellable : true ,
209212 } ,
210213 ) ;
@@ -228,6 +231,65 @@ export class Commands {
228231 ) ;
229232 }
230233
234+ public async supportBundle ( item ?: OpenableTreeItem ) : Promise < void > {
235+ const resolved = await this . resolveClientAndWorkspace ( item ) ;
236+ if ( ! resolved ) {
237+ return ;
238+ }
239+
240+ const { client, workspaceId } = resolved ;
241+
242+ const defaultName = `coder-support-${ Math . floor ( Date . now ( ) / 1000 ) } .zip` ;
243+ const outputUri = await vscode . window . showSaveDialog ( {
244+ defaultUri : vscode . Uri . file ( path . join ( os . homedir ( ) , defaultName ) ) ,
245+ filters : { "Zip files" : [ "zip" ] } ,
246+ title : "Save Support Bundle" ,
247+ } ) ;
248+ if ( ! outputUri ) {
249+ return ;
250+ }
251+ const outputPath = outputUri . fsPath ;
252+
253+ const result = await withCancellableProgress (
254+ async ( { signal, progress } ) => {
255+ progress . report ( { message : "Resolving CLI..." } ) ;
256+ const env = await this . resolveCliEnv ( client ) ;
257+ if ( ! env . featureSet . supportBundle ) {
258+ throw new Error (
259+ "Support bundles require Coder CLI v2.10.0 or later. Please update your Coder deployment." ,
260+ ) ;
261+ }
262+ progress . report ( { message : "Collecting diagnostics..." } ) ;
263+ return cliExec . supportBundle ( env , workspaceId , outputPath , signal ) ;
264+ } ,
265+ {
266+ location : vscode . ProgressLocation . Notification ,
267+ title : `Creating support bundle for ${ workspaceId } ` ,
268+ cancellable : true ,
269+ } ,
270+ ) ;
271+
272+ if ( result . ok ) {
273+ const action = await vscode . window . showInformationMessage (
274+ `Support bundle saved to ${ outputPath } ` ,
275+ "Reveal in File Explorer" ,
276+ ) ;
277+ if ( action === "Reveal in File Explorer" ) {
278+ await vscode . commands . executeCommand ( "revealFileInOS" , outputUri ) ;
279+ }
280+ return ;
281+ }
282+
283+ if ( result . cancelled ) {
284+ return ;
285+ }
286+
287+ this . logger . error ( "Support bundle failed" , result . error ) ;
288+ vscode . window . showErrorMessage (
289+ `Support bundle failed: ${ toError ( result . error ) . message } ` ,
290+ ) ;
291+ }
292+
231293 /**
232294 * View the logs for the currently connected workspace.
233295 */
@@ -720,8 +782,10 @@ export class Commands {
720782 location : vscode . ProgressLocation . Notification ,
721783 title : `Starting ping for ${ workspaceId } ...` ,
722784 } ,
723- async ( ) => {
785+ async ( progress ) => {
786+ progress . report ( { message : "Resolving CLI..." } ) ;
724787 const env = await this . resolveCliEnv ( client ) ;
788+ progress . report ( { message : "Starting..." } ) ;
725789 cliExec . ping ( env , workspaceId ) ;
726790 } ,
727791 ) ;
@@ -763,7 +827,9 @@ export class Commands {
763827 }
764828
765829 /** Resolve a CliEnv, preferring a locally cached binary over a network fetch. */
766- private async resolveCliEnv ( client : CoderApi ) : Promise < cliExec . CliEnv > {
830+ private async resolveCliEnv (
831+ client : CoderApi ,
832+ ) : Promise < cliExec . CliEnv & { featureSet : FeatureSet } > {
767833 const baseUrl = client . getAxiosInstance ( ) . defaults . baseURL ;
768834 if ( ! baseUrl ) {
769835 throw new Error ( "You are not logged in" ) ;
@@ -780,7 +846,7 @@ export class Commands {
780846 const configDir = this . pathResolver . getGlobalConfigDir ( safeHost ) ;
781847 const configs = vscode . workspace . getConfiguration ( ) ;
782848 const auth = resolveCliAuth ( configs , featureSet , baseUrl , configDir ) ;
783- return { binary, configs, auth } ;
849+ return { binary, configs, auth, featureSet } ;
784850 }
785851
786852 /**
0 commit comments