Skip to content

Commit eeaab9a

Browse files
feat: Add xcodes resource (#71)
* feat: Add xcodes resource (auto-generated from issue #70) * feat: added ability to supply appleId + password. Added completions * chore: bump versions --------- Co-authored-by: kevinwang5658 <20214115+kevinwang5658@users.noreply.github.com> Co-authored-by: kevinwang <kevinwang5658@gmail.com>
1 parent 484de42 commit eeaab9a

9 files changed

Lines changed: 370 additions & 61 deletions

File tree

completions-cron/src/__generated__/completions-index.ts

Lines changed: 62 additions & 60 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
title: xcodes
3+
description: Reference page for the xcodes resource
4+
---
5+
6+
The xcodes resource installs the [xcodes CLI](https://github.com/XcodesOrg/xcodes) tool and manages multiple Xcode versions on macOS. xcodes is the recommended way for iOS/macOS teams to ensure all developers are running the same Xcode version.
7+
8+
Installing Xcode versions requires an Apple Developer account. xcodes will prompt for Apple ID credentials interactively on first use and caches them in the macOS Keychain. For non-interactive environments, supply `appleId` and `appSpecificPassword` directly in the resource config.
9+
10+
## Parameters
11+
12+
- **xcodeVersions**: *(string[])* List of Xcode versions to install (e.g. `["15.4", "14.3.1"]`). Version strings match what `xcodes list` shows — stable versions use a dotted number (`15.4`), beta/RC versions include the label (`15 Beta 3`).
13+
- **selected**: *(string)* The active Xcode version to use, equivalent to running `xcodes select`. Must be one of the installed `xcodeVersions`.
14+
- **appleId**: *(string, optional)* Apple ID email used to authenticate with Apple's servers when downloading Xcode. If omitted, xcodes will prompt interactively.
15+
- **appleIdPassword**: *(string, optional)* Apple ID password. Required alongside `appleId` for non-interactive installs.
16+
17+
## Example usage
18+
19+
```json title="codify.jsonc"
20+
[
21+
{
22+
"type": "xcodes",
23+
"xcodeVersions": ["15.4"],
24+
"selected": "15.4",
25+
"os": ["macOS"]
26+
}
27+
]
28+
```
29+
30+
Multiple versions side by side:
31+
32+
```json title="codify.jsonc"
33+
[
34+
{
35+
"type": "xcodes",
36+
"xcodeVersions": ["14.3.1", "15.4"],
37+
"selected": "15.4",
38+
"os": ["macOS"]
39+
}
40+
]
41+
```
42+
43+
## Authentication
44+
45+
xcodes requires Apple ID credentials to download Xcode from Apple's servers. On first install Codify will prompt for your credentials interactively (including two-factor authentication). The credentials are stored in the macOS Keychain for future use.
46+
47+
For CI or fully non-interactive environments, add `appleId` and `appSpecificPassword` to the resource config:
48+
49+
```json title="codify.jsonc"
50+
[
51+
{
52+
"type": "xcodes",
53+
"xcodeVersions": ["15.4"],
54+
"selected": "15.4",
55+
"appleId": "your@apple.id",
56+
"appleIdPassword": "<Replace me here!>",
57+
"os": ["macOS"]
58+
}
59+
]
60+
```
61+
62+
Note that 2FA will still trigger interactively even with credentials set — xcodes does not support fully headless 2FA bypass.

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.13.0",
3+
"version": "1.14.0",
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/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import { CursorResource } from './resources/cursor/cursor.js';
6868
import { VscodeResource } from './resources/vscode/vscode.js';
6969
import { WebStormResource } from './resources/webstorm/webstorm.js';
7070
import { XcodeToolsResource } from './resources/xcode-tools/xcode-tools.js';
71+
import { XcodesResource } from './resources/xcodes/xcodes-resource.js';
7172
import { YumResource } from './resources/yum/yum.js';
7273
import { PyCharmResource } from './resources/jetbrains/pycharm/pycharm.js';
7374
import { ClionResource } from './resources/jetbrains/clion/clion.js';
@@ -86,6 +87,7 @@ runPlugin(Plugin.create(
8687
[
8788
new GitResource(),
8889
new XcodeToolsResource(),
90+
new XcodesResource(),
8991
new PathResource(),
9092
new AliasResource(),
9193
new AliasesResource(),
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const XCODE_RELEASES_URL = 'https://xcodereleases.com/data.json';
2+
3+
interface XcodeRelease {
4+
version: {
5+
number: string;
6+
release: { release?: boolean; beta?: number; rc?: number; dp?: number };
7+
};
8+
}
9+
10+
function toXcodesVersionString(release: XcodeRelease): string {
11+
const { number, release: rel } = release.version;
12+
if (rel.release) return number;
13+
if (rel.beta != null) return `${number} Beta ${rel.beta}`;
14+
if (rel.rc != null) return `${number} RC ${rel.rc}`;
15+
if (rel.dp != null) return `${number} DP ${rel.dp}`;
16+
return number;
17+
}
18+
19+
export default async function loadXcodeVersions(): Promise<string[]> {
20+
const response = await fetch(XCODE_RELEASES_URL);
21+
const releases = await response.json() as XcodeRelease[];
22+
return releases.map(toXcodesVersionString);
23+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { getPty, ParameterSetting, SpawnStatus, StatefulParameter } from '@codifycli/plugin-core';
2+
3+
import { XcodesConfig } from './xcodes-resource.js';
4+
5+
export class XcodesSelectedParameter extends StatefulParameter<XcodesConfig, string> {
6+
getSettings(): ParameterSetting {
7+
return {
8+
type: 'version',
9+
};
10+
}
11+
12+
override async refresh(): Promise<string | null> {
13+
const $ = getPty();
14+
const { data, status } = await $.spawnSafe('xcodes installed');
15+
if (status === SpawnStatus.ERROR) return null;
16+
return parseSelectedVersion(data);
17+
}
18+
19+
override async add(version: string): Promise<void> {
20+
const $ = getPty();
21+
await $.spawn(`xcodes select "${version}"`, { interactive: true, stdin: true });
22+
}
23+
24+
override async modify(newVersion: string): Promise<void> {
25+
const $ = getPty();
26+
await $.spawn(`xcodes select "${newVersion}"`, { interactive: true, stdin: true });
27+
}
28+
29+
override async remove(): Promise<void> {
30+
const $ = getPty();
31+
await $.spawn('xcode-select --reset', { requiresRoot: true });
32+
}
33+
}
34+
35+
function parseSelectedVersion(output: string): string | null {
36+
for (const line of output.split('\n')) {
37+
if (line.includes('Selected')) {
38+
const match = line.trim().match(/^(.+?)\s+\([^)]+\)/);
39+
return match ? match[1].trim() : null;
40+
}
41+
}
42+
return null;
43+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ArrayStatefulParameter, Plan, getPty } from '@codifycli/plugin-core';
2+
3+
import { XcodesConfig } from './xcodes-resource.js';
4+
5+
export class XcodeVersionsParameter extends ArrayStatefulParameter<XcodesConfig, string> {
6+
override async refresh(_desired: string[] | null): Promise<string[] | null> {
7+
const $ = getPty();
8+
const { data } = await $.spawnSafe('xcodes installed');
9+
return parseInstalledVersions(data);
10+
}
11+
12+
override async addItem(version: string, plan: Plan<XcodesConfig>): Promise<void> {
13+
const $ = getPty();
14+
const { appleId, appleIdPassword } = plan.desiredConfig ?? {};
15+
16+
const env: Record<string, string> = {};
17+
if (appleId) env['XCODES_USERNAME'] = appleId;
18+
if (appleIdPassword) env['XCODES_PASSWORD'] = appleIdPassword;
19+
20+
await $.spawn(`xcodes install "${version}"`, {
21+
interactive: true,
22+
stdin: true,
23+
...(Object.keys(env).length > 0 ? { env } : {}),
24+
});
25+
}
26+
27+
override async removeItem(version: string): Promise<void> {
28+
const $ = getPty();
29+
await $.spawn(`xcodes uninstall "${version}"`, { interactive: true });
30+
}
31+
}
32+
33+
function parseInstalledVersions(output: string): string[] {
34+
return output
35+
.split('\n')
36+
.map((line) => line.trim())
37+
.filter(Boolean)
38+
.map((line) => {
39+
const match = line.match(/^(.+?)\s+\([^)]+\)/);
40+
return match ? match[1].trim() : null;
41+
})
42+
.filter((v): v is string => v !== null);
43+
}

0 commit comments

Comments
 (0)