Skip to content

Commit fc6a49e

Browse files
author
test2
committed
feat: add latest parameter for xcodes and moved accept license check after
1 parent f2ff839 commit fc6a49e

5 files changed

Lines changed: 74 additions & 57 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "default",
3-
"version": "1.15.3-beta.3",
3+
"version": "1.15.3-beta.6",
44
"description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux",
55
"main": "dist/index.js",
66
"scripts": {

src/resources/xcodes/selected-parameter.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { getPty, ParameterSetting, SpawnStatus, StatefulParameter } from '@codifycli/plugin-core';
1+
import { getPty, ParameterSetting, Plan, SpawnStatus, StatefulParameter } from '@codifycli/plugin-core';
22

33
import { XcodesConfig } from './xcodes-resource.js';
4+
import { LATEST_VERSION_KEYWORD, resolveInstalledVersion } from './xcodes-utils.js';
45

56
export 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

3565
function parseSelectedVersion(output: string): string | null {

src/resources/xcodes/xcode-versions-parameter.ts

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { ArrayParameterSetting, ArrayStatefulParameter, Plan, SpawnStatus, getPty } from '@codifycli/plugin-core';
1+
import { ArrayParameterSetting, ArrayStatefulParameter, Plan, getPty } from '@codifycli/plugin-core';
22

33
import { XcodesConfig } from './xcodes-resource.js';
4-
5-
export const LATEST_VERSION_KEYWORD = 'latest';
4+
import { LATEST_VERSION_KEYWORD, parseInstalledVersions, resolveInstalledVersion } from './xcodes-utils.js';
65

76
export class XcodeVersionsParameter extends ArrayStatefulParameter<XcodesConfig, string> {
87
getSettings(): ArrayParameterSetting {
@@ -24,7 +23,7 @@ export class XcodeVersionsParameter extends ArrayStatefulParameter<XcodesConfig,
2423

2524
override async addItem(version: string, plan: Plan<XcodesConfig>): Promise<void> {
2625
const $ = getPty();
27-
const { appleId, appleIdPassword, acceptLicense } = plan.desiredConfig ?? {};
26+
const { appleId, appleIdPassword } = plan.desiredConfig ?? {};
2827

2928
const env: Record<string, string> = {};
3029
if (appleId) env['XCODES_USERNAME'] = appleId;
@@ -36,57 +35,16 @@ export class XcodeVersionsParameter extends ArrayStatefulParameter<XcodesConfig,
3635
stdin: true,
3736
...(Object.keys(env).length > 0 ? { env } : {}),
3837
});
39-
40-
if (acceptLicense !== false) {
41-
const installedVersion = await this.resolveInstalledVersion(version);
42-
if (installedVersion) await this.acceptLicenseIfNeeded(installedVersion);
43-
}
44-
}
45-
46-
private async resolveInstalledVersion(version: string): Promise<string | null> {
47-
if (version !== LATEST_VERSION_KEYWORD) return version;
48-
49-
const $ = getPty();
50-
const { data } = await $.spawnSafe('xcodes installed');
51-
const installed = parseInstalledVersions(data);
52-
return installed.at(-1) ?? null;
53-
}
54-
55-
private async acceptLicenseIfNeeded(version: string): Promise<void> {
56-
const $ = getPty();
57-
58-
// xcodebuild resolves against whatever xcode-select currently points at. If it's
59-
// still pointing at a CommandLineTools-only instance (e.g. installed before xcodes
60-
// ran), `xcodebuild -license accept` fails with "requires Xcode" even though a full
61-
// Xcode was just installed above. Explicitly select the version we just installed
62-
// first so xcode-select points at the full Xcode.
63-
await $.spawn(`xcodes select "${version}"`, { interactive: true, stdin: true });
64-
65-
const { status } = await $.spawnSafe('xcodebuild -license status');
66-
if (status === SpawnStatus.SUCCESS) return;
67-
await $.spawn('xcodebuild -license accept', { requiresRoot: true });
6838
}
6939

7040
override async removeItem(version: string): Promise<void> {
7141
const $ = getPty();
72-
const installedVersion = await this.resolveInstalledVersion(version);
42+
const installedVersion = await resolveInstalledVersion(version);
7343
if (!installedVersion) return;
7444
await $.spawn(`xcodes uninstall "${installedVersion}"`, { interactive: true });
7545
}
7646
}
7747

78-
function parseInstalledVersions(output: string): string[] {
79-
return output
80-
.split('\n')
81-
.map((line) => line.trim())
82-
.filter(Boolean)
83-
.map((line) => {
84-
const match = line.match(/^(.+?)\s+\([^)]+\)/);
85-
return match ? match[1].trim() : null;
86-
})
87-
.filter((v): v is string => v !== null);
88-
}
89-
9048
/**
9149
* Replaces whichever installed version fulfills the "latest" sentinel with the
9250
* literal string "latest" so the framework's equality check (desired === current)

src/resources/xcodes/xcodes-resource.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ const schema = z
2626
.string()
2727
.describe(
2828
'The active Xcode version to select (e.g. "15.2"). ' +
29-
'Must be one of the installed xcodeVersions. Equivalent to running xcodes select.'
29+
'Must be one of the installed xcodeVersions. Equivalent to running xcodes select. ' +
30+
'Use "latest" to select the newest installed Xcode version.'
3031
)
3132
.optional(),
3233
appleId: z
@@ -46,8 +47,8 @@ const schema = z
4647
.boolean()
4748
.optional()
4849
.describe(
49-
'Automatically accept the Xcode license agreement after installation. ' +
50-
'Runs `sudo xcodebuild -license accept`. Defaults to true.'
50+
'Automatically accept the Xcode license agreement after selecting an Xcode version. ' +
51+
'Runs `sudo xcodebuild -license accept`. Only applies when `selected` is set. Defaults to true.'
5152
),
5253
})
5354
.describe('xcodes resource — install and manage multiple Xcode versions via the xcodes CLI');
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getPty } from '@codifycli/plugin-core';
2+
3+
export const LATEST_VERSION_KEYWORD = 'latest';
4+
5+
export function parseInstalledVersions(output: string): string[] {
6+
return output
7+
.split('\n')
8+
.map((line) => line.trim())
9+
.filter(Boolean)
10+
.map((line) => {
11+
const match = line.match(/^(.+?)\s+\([^)]+\)/);
12+
return match ? match[1].trim() : null;
13+
})
14+
.filter((v): v is string => v !== null);
15+
}
16+
17+
/**
18+
* Resolves the "latest" sentinel to the newest installed Xcode version.
19+
* Returns the input unchanged if it isn't the "latest" sentinel.
20+
*/
21+
export async function resolveInstalledVersion(version: string): Promise<string | null> {
22+
if (version !== LATEST_VERSION_KEYWORD) return version;
23+
24+
const $ = getPty();
25+
const { data } = await $.spawnSafe('xcodes installed', { interactive: true });
26+
const installed = parseInstalledVersions(data);
27+
return installed.at(-1) ?? null;
28+
}

0 commit comments

Comments
 (0)