22// Licensed under the MIT License.
33
44import * as fse from 'fs-extra' ;
5+ import * as os from 'os' ;
56import * as path from 'path' ;
67import { traceError , traceInfo , traceVerbose } from '../../common/logging' ;
78import { isWindows } from '../../common/utils/platformUtils' ;
@@ -17,6 +18,20 @@ export interface ShellSourcingScripts {
1718 sh ?: string ;
1819 /** Windows CMD batch file (activate.bat) */
1920 cmd ?: string ;
21+ /** Fish shell initialization script (conda.fish) */
22+ fish ?: string ;
23+ }
24+
25+ /**
26+ * Tracks whether `conda init <shell>` has been run for each shell type.
27+ * When true, the shell's profile/config file contains the conda initialization block,
28+ * meaning bare `conda` will be available as a shell function when that shell starts.
29+ */
30+ export interface ShellCondaInitStatus {
31+ bash ?: boolean ;
32+ zsh ?: boolean ;
33+ fish ?: boolean ;
34+ pwsh ?: boolean ;
2035}
2136
2237/**
@@ -37,6 +52,7 @@ export class CondaSourcingStatus {
3752 public isActiveOnLaunch ?: boolean ,
3853 public globalSourcingScript ?: string ,
3954 public shellSourcingScripts ?: ShellSourcingScripts ,
55+ public shellInitStatus ?: ShellCondaInitStatus ,
4056 ) { }
4157
4258 /**
@@ -59,6 +75,7 @@ export class CondaSourcingStatus {
5975 scripts . ps1 && `PowerShell: ${ scripts . ps1 } ` ,
6076 scripts . sh && `Bash/sh: ${ scripts . sh } ` ,
6177 scripts . cmd && `CMD: ${ scripts . cmd } ` ,
78+ scripts . fish && `Fish: ${ scripts . fish } ` ,
6279 ] . filter ( Boolean ) ;
6380
6481 if ( entries . length > 0 ) {
@@ -74,6 +91,13 @@ export class CondaSourcingStatus {
7491 lines . push ( '└─ No Shell-specific Sourcing Scripts Found' ) ;
7592 }
7693
94+ if ( this . shellInitStatus ) {
95+ const initEntries = ( [ 'bash' , 'zsh' , 'fish' , 'pwsh' ] as const )
96+ . map ( ( s ) => `${ s } : ${ this . shellInitStatus ! [ s ] ? '✓' : '✗' } ` )
97+ . join ( ', ' ) ;
98+ lines . push ( `├─ Shell conda init status: ${ initEntries } ` ) ;
99+ }
100+
77101 return lines . join ( '\n' ) ;
78102 }
79103}
@@ -116,6 +140,9 @@ export async function constructCondaSourcingStatus(condaPath: string): Promise<C
116140 // find and save all of the shell specific sourcing scripts
117141 sourcingStatus . shellSourcingScripts = await findShellSourcingScripts ( sourcingStatus ) ;
118142
143+ // check shell profile files to see if `conda init <shell>` has been run
144+ sourcingStatus . shellInitStatus = await checkCondaInitInShellProfiles ( ) ;
145+
119146 return sourcingStatus ;
120147}
121148
@@ -148,6 +175,7 @@ export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStat
148175 let ps1Script : string | undefined ;
149176 let shScript : string | undefined ;
150177 let cmdActivate : string | undefined ;
178+ let fishScript : string | undefined ;
151179
152180 try {
153181 // Search for PowerShell hook script (conda-hook.ps1)
@@ -178,20 +206,103 @@ export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStat
178206 } catch ( err ) {
179207 logs . push ( ` Error during CMD script search: ${ err instanceof Error ? err . message : 'Unknown error' } ` ) ;
180208 }
209+
210+ // Search for Fish shell script (conda.fish)
211+ logs . push ( '\nSearching for Fish shell script...' ) ;
212+ try {
213+ fishScript = await getCondaFishPath ( sourcingStatus . condaFolder ) ;
214+ logs . push ( ` Path: ${ fishScript ?? '✗ Not found' } ` ) ;
215+ } catch ( err ) {
216+ logs . push ( ` Error during Fish script search: ${ err instanceof Error ? err . message : 'Unknown error' } ` ) ;
217+ }
181218 } catch ( error ) {
182219 logs . push ( `\nCritical error during script search: ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
183220 } finally {
184221 logs . push ( '\nSearch Summary:' ) ;
185222 logs . push ( ` PowerShell: ${ ps1Script ? '✓' : '✗' } ` ) ;
186223 logs . push ( ` Shell: ${ shScript ? '✓' : '✗' } ` ) ;
187224 logs . push ( ` CMD: ${ cmdActivate ? '✓' : '✗' } ` ) ;
225+ logs . push ( ` Fish: ${ fishScript ? '✓' : '✗' } ` ) ;
188226 logs . push ( '============================' ) ;
189227
190228 // Log everything at once
191229 traceVerbose ( logs . join ( '\n' ) ) ;
192230 }
193231
194- return { ps1 : ps1Script , sh : shScript , cmd : cmdActivate } ;
232+ return { ps1 : ps1Script , sh : shScript , cmd : cmdActivate , fish : fishScript } ;
233+ }
234+
235+ /**
236+ * Checks shell profile/config files to determine if `conda init <shell>` has been run.
237+ *
238+ * When `conda init <shell>` is run, it adds a `# >>> conda initialize >>>` block to the
239+ * shell's profile. If that block is present, then any new terminal of that shell type will
240+ * have `conda` available as a shell function, and bare `conda activate` will work.
241+ *
242+ * For Fish, `conda init fish` may either modify `config.fish` or drop a file in
243+ * `~/.config/fish/conf.d/`, so both locations are checked.
244+ *
245+ * @param homeDir Optional home directory override (defaults to os.homedir(), useful for testing)
246+ * @returns Status object indicating which shells have conda initialized
247+ */
248+ export async function checkCondaInitInShellProfiles ( homeDir ?: string ) : Promise < ShellCondaInitStatus > {
249+ const home = homeDir ?? os . homedir ( ) ;
250+ const status : ShellCondaInitStatus = { } ;
251+ const logs : string [ ] = [ '=== Checking shell profiles for conda init ===' ] ;
252+
253+ const checks : Array < { shell : keyof ShellCondaInitStatus ; files : string [ ] } > = [
254+ {
255+ shell : 'bash' ,
256+ files : [ path . join ( home , '.bashrc' ) , path . join ( home , '.bash_profile' ) ] ,
257+ } ,
258+ {
259+ shell : 'zsh' ,
260+ files : [ path . join ( home , '.zshrc' ) ] ,
261+ } ,
262+ {
263+ shell : 'fish' ,
264+ files : [
265+ path . join ( process . env . XDG_CONFIG_HOME || path . join ( home , '.config' ) , 'fish' , 'config.fish' ) ,
266+ path . join ( process . env . XDG_CONFIG_HOME || path . join ( home , '.config' ) , 'fish' , 'conf.d' , 'conda.fish' ) ,
267+ ] ,
268+ } ,
269+ {
270+ shell : 'pwsh' ,
271+ files : [
272+ path . join (
273+ process . env . XDG_CONFIG_HOME || path . join ( home , '.config' ) ,
274+ 'powershell' ,
275+ 'Microsoft.PowerShell_profile.ps1' ,
276+ ) ,
277+ path . join ( process . env . XDG_CONFIG_HOME || path . join ( home , '.config' ) , 'powershell' , 'profile.ps1' ) ,
278+ ] ,
279+ } ,
280+ ] ;
281+
282+ await Promise . all (
283+ checks . map ( async ( { shell, files } ) => {
284+ for ( const filePath of files ) {
285+ try {
286+ if ( await fse . pathExists ( filePath ) ) {
287+ const content = await fse . readFile ( filePath , 'utf-8' ) ;
288+ if ( content . includes ( 'conda initialize' ) ) {
289+ status [ shell ] = true ;
290+ logs . push ( ` ${ shell } : ✓ conda init found in ${ filePath } ` ) ;
291+ return ;
292+ }
293+ }
294+ } catch {
295+ // File not readable, skip
296+ }
297+ }
298+ logs . push ( ` ${ shell } : ✗ conda init not found` ) ;
299+ } ) ,
300+ ) ;
301+
302+ logs . push ( '============================' ) ;
303+ traceVerbose ( logs . join ( '\n' ) ) ;
304+
305+ return status ;
195306}
196307
197308/**
@@ -308,6 +419,24 @@ async function getCondaShPath(condaFolder: string): Promise<string | undefined>
308419 return shPathPromise ;
309420}
310421
422+ /**
423+ * Returns the path to conda.fish given a conda installation folder.
424+ *
425+ * Searches for conda.fish in these locations (relative to the conda root):
426+ * - etc/fish/conf.d/conda.fish
427+ * - shell/etc/fish/conf.d/conda.fish
428+ * - Library/etc/fish/conf.d/conda.fish
429+ */
430+ export async function getCondaFishPath ( condaFolder : string ) : Promise < string | undefined > {
431+ const locations = [
432+ path . join ( condaFolder , 'etc' , 'fish' , 'conf.d' , 'conda.fish' ) ,
433+ path . join ( condaFolder , 'shell' , 'etc' , 'fish' , 'conf.d' , 'conda.fish' ) ,
434+ path . join ( condaFolder , 'Library' , 'etc' , 'fish' , 'conf.d' , 'conda.fish' ) ,
435+ ] ;
436+
437+ return findFileInLocations ( locations , 'conda.fish' ) ;
438+ }
439+
311440/**
312441 * Returns the path to the Windows batch activation file (activate.bat) for conda
313442 * @param condaPath The path to the conda executable
0 commit comments