33 DestroyPlan ,
44 getPty ,
55 ModifyPlan ,
6- ParameterChange ,
6+ ParameterChange , RefreshContext , resolvePathWithVariables ,
77 Resource ,
88 ResourceSettings
99} from 'codify-plugin-lib' ;
@@ -20,26 +20,44 @@ export interface PathConfig extends StringIndexedObject {
2020 path : string ;
2121 paths : string [ ] ;
2222 prepend : boolean ;
23+ declarationsOnly : boolean ;
2324}
2425
2526export class PathResource extends Resource < PathConfig > {
27+ private readonly PATH_DECLARATION_REGEX = / ( ( e x p o r t P A T H = ) | ( p a t h + = \( ) | ( p a t h = \( ) ) ( .+ ?) [ \n ; ] / g;
28+ private readonly PATH_REGEX = / (?< = [ = " ' : ( ] ) ( [ ^ " ' \n \r ] + ?) (? = [ " ' : ) \n ; ] ) / g
29+ private readonly filePaths = [
30+ path . join ( os . homedir ( ) , '.zshrc' ) ,
31+ path . join ( os . homedir ( ) , '.zprofile' ) ,
32+ path . join ( os . homedir ( ) , '.zshenv' ) ,
33+ ]
34+
2635 getSettings ( ) : ResourceSettings < PathConfig > {
2736 return {
2837 id : 'path' ,
2938 schema : Schema ,
3039 parameterSettings : {
3140 path : { type : 'directory' } ,
3241 paths : { canModify : true , type : 'array' , itemType : 'directory' } ,
33- prepend : { default : false }
42+ prepend : { default : false , setting : true } ,
43+ declarationsOnly : { default : false , setting : true } ,
3444 } ,
3545 importAndDestroy :{
36- refreshKeys : [ 'paths' ] ,
46+ refreshKeys : [ 'paths' , 'declarationsOnly' ] ,
3747 defaultRefreshValues : {
38- paths : [ ]
48+ paths : [ ] ,
49+ declarationsOnly : true ,
3950 }
4051 } ,
4152 allowMultiple : {
42- identifyingParameters : [ 'path' , 'paths' ]
53+ matcher : ( desired , current ) => {
54+ if ( desired . path ) {
55+ return desired . path === current . path ;
56+ }
57+
58+ const currentPaths = new Set ( current . paths )
59+ return desired . paths ?. some ( ( p ) => currentPaths . has ( p ) ) ;
60+ }
4361 }
4462 }
4563 }
@@ -50,11 +68,47 @@ export class PathResource extends Resource<PathConfig> {
5068 }
5169 }
5270
53- override async refresh ( parameters : Partial < PathConfig > ) : Promise < Partial < PathConfig > | null > {
54- const $ = getPty ( ) ;
71+ override async refresh ( parameters : Partial < PathConfig > , context : RefreshContext < PathConfig > ) : Promise < Partial < PathConfig > | null > {
72+ // If declarations only, we only look into files to find potential paths
73+ if ( parameters . declarationsOnly || context . isStateful ) {
74+ const pathsResult = new Set < string > ( ) ;
75+
76+ for ( const path of this . filePaths ) {
77+ if ( ! ( await FileUtils . fileExists ( path ) ) ) {
78+ continue ;
79+ }
80+
81+ const contents = await fs . readFile ( path , 'utf8' ) ;
82+ const pathDeclarations = this . findAllPathDeclarations ( contents ) ;
5583
84+ if ( parameters . path && pathDeclarations . some ( ( d ) => resolvePathWithVariables ( untildify ( d . path ) ) === parameters . path ) ) {
85+ return parameters ;
86+ }
87+
88+ if ( parameters . paths ) {
89+ pathDeclarations
90+ . map ( ( d ) => d . path )
91+ . forEach ( ( d ) => pathsResult . add ( resolvePathWithVariables ( untildify ( d ) ) ) ) ;
92+ }
93+ }
94+
95+ if ( parameters . path || pathsResult . size === 0 ) {
96+ return null ;
97+ }
98+
99+ return {
100+ ...parameters ,
101+ paths : [ ...pathsResult ] ,
102+ }
103+ }
104+
105+ // Otherwise look in path variable to see if it exists
106+ const $ = getPty ( ) ;
56107 const { data : existingPaths } = await $ . spawnSafe ( 'echo $PATH' )
57- if ( parameters . path !== undefined && ( existingPaths . includes ( parameters . path ) || existingPaths . includes ( untildify ( parameters . path ) ) ) ) {
108+
109+ if ( parameters . path !== undefined && (
110+ existingPaths . includes ( parameters . path )
111+ ) ) {
58112 return parameters ;
59113 }
60114
@@ -73,8 +127,8 @@ export class PathResource extends Resource<PathConfig> {
73127 const userPaths = existingPaths . split ( ':' )
74128 . filter ( ( p ) => ! systemPaths . includes ( p ) )
75129
76- if ( parameters . paths !== undefined ) {
77- return { paths : userPaths , prepend : parameters . prepend } ;
130+ if ( parameters . paths && userPaths . length > 0 ) {
131+ return { ... parameters , paths : userPaths } ;
78132 }
79133
80134 return null ;
@@ -131,68 +185,63 @@ export class PathResource extends Resource<PathConfig> {
131185 }
132186
133187 private async removePath ( pathValue : string ) : Promise < void > {
134- const fileInfo = await this . findPathDeclaration ( pathValue ) ;
135- if ( ! fileInfo ) {
188+ const foundPaths = await this . findPath ( pathValue ) ;
189+ if ( foundPaths . length === 0 ) {
136190 throw new Error ( `Could not find path declaration: ${ pathValue } . Please manually remove the path and then re-run Codify` ) ;
137191 }
138192
139- const { content, pathsFound, filePath } = fileInfo ;
140-
141- const fileLines = content
142- . split ( / \n / ) ;
193+ for ( const foundPath of foundPaths ) {
194+ console . log ( `Removing path: ${ pathValue } from ${ foundPath . file } ` )
195+ await FileUtils . removeFromFile ( foundPath . file , foundPath . pathDeclaration . declaration ) ;
196+ }
197+ }
143198
144- for ( const pathFound of pathsFound ) {
145- const line = fileLines
146- . findIndex ( ( l ) => l . includes ( pathFound ) ) ;
199+ private async findPath ( pathToFind : string ) : Promise < Array < { file : string ; pathDeclaration : PathDeclaration } > > {
200+ const result = [ ] ;
147201
148- if ( line === - 1 ) {
149- throw new Error ( `Could not find path declaration: ${ pathValue } . Please manually remove the path and then re-run Codify` ) ;
202+ for ( const filePath of this . filePaths ) {
203+ if ( ! ( await FileUtils . fileExists ( filePath ) ) ) {
204+ continue ;
150205 }
151206
152- fileLines . splice ( line , 1 ) ;
207+ const contents = await fs . readFile ( filePath , 'utf8' ) ;
208+ const pathDeclarations = this . findAllPathDeclarations ( contents ) ;
209+
210+ const foundDeclarations = pathDeclarations . filter ( ( d ) => d . path === pathToFind ) ;
211+ result . push ( ...foundDeclarations . map ( ( d ) => ( { pathDeclaration : d , file : filePath } ) ) ) ;
153212 }
154213
155- console . log ( `Removing path: ${ pathValue } from ${ filePath } ` )
156- await fs . writeFile ( filePath , fileLines . join ( '\n' ) , { encoding : 'utf8' } ) ;
214+ return result ;
157215 }
158216
159- private async findPathDeclaration ( value : string ) : Promise < PathDeclaration | null > {
160- const filePaths = [
161- path . join ( os . homedir ( ) , '.zshrc' ) ,
162- path . join ( os . homedir ( ) , '.zprofile' ) ,
163- path . join ( os . homedir ( ) , '.zshenv' ) ,
164- ] ;
165-
166- const searchTerms = [
167- `export PATH=${ value } :$PATH` ,
168- `export PATH=$PATH:${ value } ` ,
169- `path+=('${ value } ')` ,
170- `path+=(${ value } )` ,
171- `path=('${ value } ' $path)` ,
172- `path=(${ value } $path)`
173- ]
174-
175- for ( const filePath of filePaths ) {
176- if ( await FileUtils . fileExists ( filePath ) ) {
177- const fileContents = await fs . readFile ( filePath , 'utf8' ) ;
178-
179- const pathsFound = searchTerms . filter ( ( st ) => fileContents . includes ( st ) ) ;
180- if ( pathsFound . length > 0 ) {
181- return {
182- filePath,
183- content : fileContents ,
184- pathsFound,
185- }
217+ findAllPathDeclarations ( contents : string ) : PathDeclaration [ ] {
218+ const results = [ ] ;
219+ const pathDeclarations = contents . matchAll ( this . PATH_DECLARATION_REGEX ) ;
220+
221+ for ( const declaration of pathDeclarations ) {
222+ const trimmedDeclaration = declaration [ 0 ] ;
223+ const paths = trimmedDeclaration . matchAll ( this . PATH_REGEX ) ;
224+
225+ for ( const path of paths ) {
226+ const trimmedPath = path [ 0 ] ;
227+ if ( trimmedPath === '$PATH' ) {
228+ continue ;
186229 }
230+
231+ results . push ( {
232+ declaration : trimmedDeclaration . trim ( ) ,
233+ path : trimmedPath ,
234+ } ) ;
187235 }
188236 }
189237
190- return null ;
238+ return results ;
191239 }
192240}
193241
194242interface PathDeclaration {
195- filePath : string ;
196- content : string ;
197- pathsFound : string [ ] ;
243+ // The entire declaration. Ex for: export PATH="$PYENV_ROOT/bin:$PATH", it's export PATH="$PYENV_ROOT/bin:$PATH"
244+ declaration : string ;
245+ // The path being added. Ex for: export PATH="$PYENV_ROOT/bin:$PATH", it's $PYENV_ROOT/bin
246+ path : string ;
198247}
0 commit comments