1- import { getPty , ParameterSetting , SpawnStatus , StatefulParameter } from '@codifycli/plugin-core' ;
1+ import { getPty , ParameterSetting , Plan , SpawnStatus , StatefulParameter } from '@codifycli/plugin-core' ;
22
33import { XcodesConfig } from './xcodes-resource.js' ;
4+ import { LATEST_VERSION_KEYWORD , resolveInstalledVersion } from './xcodes-utils.js' ;
45
56export class XcodesSelectedParameter extends StatefulParameter < XcodesConfig , string > {
67 getSettings ( ) : ParameterSetting {
@@ -9,27 +10,56 @@ export class XcodesSelectedParameter extends StatefulParameter<XcodesConfig, str
910 } ;
1011 }
1112
12- override async refresh ( ) : Promise < string | null > {
13+ override async refresh ( desired : string | null ) : Promise < string | null > {
1314 const $ = getPty ( ) ;
1415 const { data, status } = await $ . spawnSafe ( 'xcodes installed' ) ;
1516 if ( status === SpawnStatus . ERROR ) return null ;
16- return parseSelectedVersion ( data ) ;
17+ const selected = parseSelectedVersion ( data ) ;
18+
19+ // "latest" isn't a real xcode-select target — normalize the currently selected
20+ // version back to the literal "latest" when it's also the newest installed
21+ // version, so a desired value of "latest" converges instead of diffing forever.
22+ if ( desired === LATEST_VERSION_KEYWORD && selected ) {
23+ const newestInstalled = await resolveInstalledVersion ( LATEST_VERSION_KEYWORD ) ;
24+ if ( selected === newestInstalled ) return LATEST_VERSION_KEYWORD ;
25+ }
26+
27+ return selected ;
1728 }
1829
19- override async add ( version : string ) : Promise < void > {
30+ override async add ( version : string , plan : Plan < XcodesConfig > ) : Promise < void > {
2031 const $ = getPty ( ) ;
21- await $ . spawn ( `xcodes select "${ version } "` , { interactive : true , stdin : true } ) ;
32+ const resolved = await resolveInstalledVersion ( version ) ;
33+ if ( ! resolved ) throw new Error ( `Unable to resolve xcode version "${ version } " to select. Ensure it is listed in xcodeVersions.` ) ;
34+ await $ . spawn ( `xcodes select "${ resolved } "` , { interactive : true , stdin : true } ) ;
35+ await this . acceptLicenseIfNeeded ( plan ) ;
2236 }
2337
24- override async modify ( newVersion : string ) : Promise < void > {
38+ override async modify ( newVersion : string , _previousVersion : string , plan : Plan < XcodesConfig > ) : Promise < void > {
2539 const $ = getPty ( ) ;
26- await $ . spawn ( `xcodes select "${ newVersion } "` , { interactive : true , stdin : true } ) ;
40+ const resolved = await resolveInstalledVersion ( newVersion ) ;
41+ if ( ! resolved ) throw new Error ( `Unable to resolve xcode version "${ newVersion } " to select. Ensure it is listed in xcodeVersions.` ) ;
42+ await $ . spawn ( `xcodes select "${ resolved } "` , { interactive : true , stdin : true } ) ;
43+ await this . acceptLicenseIfNeeded ( plan ) ;
2744 }
2845
2946 override async remove ( ) : Promise < void > {
3047 const $ = getPty ( ) ;
3148 await $ . spawn ( 'xcode-select --reset' , { requiresRoot : true } ) ;
3249 }
50+
51+ // xcodes select only ever selects a fully-installed Xcode.app (never a
52+ // CommandLineTools-only instance, which xcodes doesn't track), so once select
53+ // succeeds above, xcode-select is guaranteed to point at a full Xcode and
54+ // xcodebuild -license accept can run safely.
55+ private async acceptLicenseIfNeeded ( plan : Plan < XcodesConfig > ) : Promise < void > {
56+ if ( plan . desiredConfig ?. acceptLicense === false ) return ;
57+
58+ const $ = getPty ( ) ;
59+ const { status } = await $ . spawnSafe ( 'xcodebuild -license status' ) ;
60+ if ( status === SpawnStatus . SUCCESS ) return ;
61+ await $ . spawn ( 'xcodebuild -license accept' , { requiresRoot : true } ) ;
62+ }
3363}
3464
3565function parseSelectedVersion ( output : string ) : string | null {
0 commit comments