@@ -2,11 +2,6 @@ import { ParameterSetting, Plan, StatefulParameter, getPty } from '@codifycli/pl
22
33import { NpmConfig } from './npm.js' ;
44
5- export interface NpmPackage {
6- name : string ;
7- version ?: string ;
8- }
9-
105interface NpmLsResponse {
116 version : string ;
127 name : string ;
@@ -17,18 +12,24 @@ interface NpmLsResponse {
1712 } > ;
1813}
1914
20- export class NpmGlobalInstallParameter extends StatefulParameter < NpmConfig , Array < NpmPackage | string > > {
15+ // Extracts the package name without the version specifier (e.g. "nodemon@3.1.10" → "nodemon")
16+ function packageName ( pkg : string ) : string {
17+ const atIndex = pkg . lastIndexOf ( '@' )
18+ return atIndex > 0 ? pkg . slice ( 0 , atIndex ) : pkg
19+ }
20+
21+ export class NpmInstallParameter extends StatefulParameter < NpmConfig , string [ ] > {
2122
2223 getSettings ( ) : ParameterSetting {
2324 return {
2425 type : 'array' ,
2526 isElementEqual : this . isEqual ,
2627 filterInStatelessMode : ( desired , current ) =>
27- current . filter ( ( c ) => desired . some ( ( d ) => this . isSamePackage ( d , c ) ) ) ,
28+ current . filter ( ( c ) => desired . some ( ( d ) => packageName ( d ) === packageName ( c ) ) ) ,
2829 }
2930 }
3031
31- async refresh ( desired : ( NpmPackage | string ) [ ] | null , config : Partial < NpmConfig > ) : Promise < ( NpmPackage | string ) [ ] | null > {
32+ async refresh ( desired : string [ ] | null , config : Partial < NpmConfig > ) : Promise < string [ ] | null > {
3233 const pty = getPty ( ) ;
3334
3435 const { data } = await pty . spawnSafe ( 'npm ls --json --global --depth=0 --loglevel=silent' )
@@ -37,106 +38,56 @@ export class NpmGlobalInstallParameter extends StatefulParameter<NpmConfig, Arra
3738 }
3839
3940 const parsedData = JSON . parse ( data ) as NpmLsResponse ;
40- const dependencies = Object . entries ( parsedData . dependencies ?? { } )
41- . map ( ( [ name , info ] ) => ( {
42- name,
43- version : info . version ,
44- } ) )
45-
46- return dependencies . map ( ( c ) => {
47- if ( desired ?. some ( ( d ) => typeof d === 'string' && d === c . name ) ) {
48- return c . name ;
49- }
5041
51- if ( desired ?. some ( ( d ) => typeof d === 'object' && d . name === c . name && ! d . version ) ) {
52- return { name : c . name } ;
42+ return Object . entries ( parsedData . dependencies ?? { } ) . map ( ( [ name , info ] ) => {
43+ // If desired entry has a version specifier, return name@version so equality checks work
44+ if ( desired ?. some ( ( d ) => d . includes ( '@' ) && packageName ( d ) === name ) ) {
45+ return `${ name } @${ info . version } `
5346 }
54-
55- return c ;
47+ return name
5648 } )
5749 }
5850
59- async add ( valueToAdd : Array < NpmPackage | string > , plan : Plan < NpmConfig > ) : Promise < void > {
51+ async add ( valueToAdd : string [ ] , plan : Plan < NpmConfig > ) : Promise < void > {
6052 await this . install ( valueToAdd ) ;
6153 }
6254
63- async modify ( newValue : ( NpmPackage | string ) [ ] , previousValue : ( NpmPackage | string ) [ ] , plan : Plan < NpmConfig > ) : Promise < void > {
64- const toInstall = newValue . filter ( ( n ) => ! previousValue . some ( ( p ) => this . isSamePackage ( n , p ) ) ) ;
65- const toUninstall = previousValue . filter ( ( p ) => ! newValue . some ( ( n ) => this . isSamePackage ( n , p ) ) ) ;
55+ async modify ( newValue : string [ ] , previousValue : string [ ] , plan : Plan < NpmConfig > ) : Promise < void > {
56+ const toInstall = newValue . filter ( ( n ) => ! previousValue . some ( ( p ) => packageName ( n ) === packageName ( p ) ) ) ;
57+ const toUninstall = previousValue . filter ( ( p ) => ! newValue . some ( ( n ) => packageName ( n ) === packageName ( p ) ) ) ;
6658
6759 if ( plan . isStateful && toUninstall . length > 0 ) {
6860 await this . uninstall ( toUninstall ) ;
6961 }
7062 await this . install ( toInstall ) ;
7163 }
7264
73- async remove ( valueToRemove : ( NpmPackage | string ) [ ] , plan : Plan < NpmConfig > ) : Promise < void > {
65+ async remove ( valueToRemove : string [ ] , plan : Plan < NpmConfig > ) : Promise < void > {
7466 await this . uninstall ( valueToRemove ) ;
7567 }
7668
77- async install ( packages : Array < NpmPackage | string > ) : Promise < void > {
78- const $ = getPty ( ) ;
79- const installStatements = packages . map ( ( p ) => {
80- if ( typeof p === 'string' ) {
81- return p ;
82- }
83-
84- if ( p . version ) {
85- return `${ p . name } @${ p . version } ` ;
86- }
87-
88- return p . name ;
89- } )
90-
91- if ( installStatements . length === 0 ) {
69+ async install ( packages : string [ ] ) : Promise < void > {
70+ if ( packages . length === 0 ) {
9271 return ;
9372 }
94-
95- await $ . spawn ( `npm install --global ${ installStatements . join ( ' ' ) } ` , { interactive : true } ) ;
96- }
97-
98- async uninstall ( packages : Array < NpmPackage | string > ) : Promise < void > {
9973 const $ = getPty ( ) ;
100- const uninstallStatements = packages . map ( ( p ) => {
101- if ( typeof p === 'string' ) {
102- return p ;
103- }
104-
105- return p . name ;
106- } )
107-
108- if ( uninstallStatements . length === 0 ) {
109- return ;
110- }
111-
112- await $ . spawn ( `npm uninstall --global ${ uninstallStatements . join ( ' ' ) } ` , { interactive : true } ) ;
74+ await $ . spawn ( `npm install --global ${ packages . join ( ' ' ) } ` , { interactive : true } ) ;
11375 }
11476
115-
116- isSamePackage ( desired : NpmPackage | string , current : NpmPackage | string ) : boolean {
117- if ( typeof desired === 'string' && typeof current === 'string' ) {
118- return desired === current ;
119- }
120-
121- if ( typeof desired === 'object' && typeof current === 'object' ) {
122- return desired . name === current . name ;
77+ async uninstall ( packages : string [ ] ) : Promise < void > {
78+ if ( packages . length === 0 ) {
79+ return ;
12380 }
124-
125- return false ;
81+ const $ = getPty ( ) ;
82+ await $ . spawn ( `npm uninstall --global ${ packages . map ( packageName ) . join ( ' ' ) } ` , { interactive : true } ) ;
12683 }
12784
128- isEqual ( desired : NpmPackage | string , current : NpmPackage | string ) : boolean {
129- if ( typeof desired === 'string' && typeof current === 'string' ) {
130- return desired === current ;
131- }
132-
133- if ( typeof desired === 'object' && typeof current === 'object' ) {
134- return desired . version
135- ? desired . name === current . name && desired . version === current . version
136- : desired . name === current . name ;
85+ isEqual ( desired : string , current : string ) : boolean {
86+ // If no version specified in desired, match by name only
87+ if ( ! desired . includes ( '@' ) || desired . startsWith ( '@' ) ) {
88+ return packageName ( desired ) === packageName ( current )
13789 }
138-
139- return false ;
90+ return desired === current
14091 }
14192
14293}
0 commit comments