@@ -86,6 +86,13 @@ export interface PlanRow {
8686 action : PlanAction ;
8787 from : string | null ;
8888 to : string | null ;
89+ /**
90+ * Where the package should end up. For install/change/keep this is the
91+ * profile entry's recorded scope, so applying installs into the same scope it
92+ * was saved from (project scope needs pi's `-l`). For `extra` rows there is
93+ * no profile entry, so it reflects where the package is currently installed.
94+ */
95+ scope : "user" | "project" ;
8996}
9097
9198/**
@@ -99,19 +106,23 @@ export function planApply(profile: Profile, installedNow: Map<string, { scope: "
99106 for ( const entry of profile . packages ) {
100107 const current = installedNow . get ( entry . name ) ;
101108 if ( ! current ) {
102- rows . push ( { name : entry . name , action : "install" , from : null , to : entry . version } ) ;
109+ rows . push ( { name : entry . name , action : "install" , from : null , to : entry . version , scope : entry . scope } ) ;
103110 continue ;
104111 }
105112 const onDisk = installedVersionOnDisk ( entry . name , current . scope ) . version ;
106- if ( entry . version && onDisk && entry . version !== onDisk ) {
107- rows . push ( { name : entry . name , action : "change" , from : onDisk , to : entry . version } ) ;
113+ // A user↔project mismatch is a change even at the same version: the package
114+ // has to be reinstalled into the scope the profile recorded.
115+ const scopeMismatch = current . scope !== entry . scope ;
116+ const versionMismatch = Boolean ( entry . version && onDisk && entry . version !== onDisk ) ;
117+ if ( scopeMismatch || versionMismatch ) {
118+ rows . push ( { name : entry . name , action : "change" , from : onDisk , to : entry . version , scope : entry . scope } ) ;
108119 } else {
109- rows . push ( { name : entry . name , action : "keep" , from : onDisk , to : entry . version } ) ;
120+ rows . push ( { name : entry . name , action : "keep" , from : onDisk , to : entry . version , scope : entry . scope } ) ;
110121 }
111122 }
112- for ( const [ name ] of installedNow ) {
123+ for ( const [ name , cur ] of installedNow ) {
113124 if ( ! profile . packages . some ( ( p ) => p . name === name ) ) {
114- rows . push ( { name, action : "extra" , from : installedVersionOnDisk ( name , "user" ) . version , to : null } ) ;
125+ rows . push ( { name, action : "extra" , from : installedVersionOnDisk ( name , cur . scope ) . version , to : null , scope : cur . scope } ) ;
115126 }
116127 }
117128 return rows . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
@@ -123,13 +134,14 @@ export interface ProfileOptions {
123134}
124135
125136function describe ( row : PlanRow ) : string {
137+ const scope = row . scope === "project" ? " [project]" : "" ;
126138 switch ( row . action ) {
127139 case "install" :
128- return `install ${ row . to ? `@ ${ row . to } ` : "(latest)" } ` ;
140+ return `install ${ row . to ? `@ ${ row . to } ` : "(latest)" } ${ scope } ` ;
129141 case "change" :
130- return `${ row . from } → ${ row . to } ` ;
142+ return `${ row . from ?? "unknown" } → ${ row . to ?? "latest" } ${ scope } ` ;
131143 case "keep" :
132- return `already ${ row . from ?? "installed" } ` ;
144+ return `already ${ row . from ?? "installed" } ${ scope } ` ;
133145 case "extra" :
134146 return `installed here, not in the profile (left alone)` ;
135147 }
@@ -189,6 +201,9 @@ export async function profile(args: string[], opts: ProfileOptions): Promise<num
189201 for ( const row of work ) {
190202 const spec = row . to ? `npm:@pify/${ row . name } @${ row . to } ` : `npm:@pify/${ row . name } ` ;
191203 const argv = [ "install" , spec ] ;
204+ // Mirror install.ts's piArgs: project-scoped entries need pi's `-l`, or
205+ // they land in the user scope instead of the one the profile recorded.
206+ if ( row . scope === "project" ) argv . push ( "-l" ) ;
192207 step ( `pi ${ argv . join ( " " ) } ` ) ;
193208 if ( ( await delegate ( argv ) ) !== 0 ) failed . push ( row . name ) ;
194209 }
0 commit comments