Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 62 additions & 60 deletions completions-cron/src/__generated__/completions-index.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions docs/resources/(resources)/xcodes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: xcodes
description: Reference page for the xcodes resource
---

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.

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.

## Parameters

- **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`).
- **selected**: *(string)* The active Xcode version to use, equivalent to running `xcodes select`. Must be one of the installed `xcodeVersions`.
- **appleId**: *(string, optional)* Apple ID email used to authenticate with Apple's servers when downloading Xcode. If omitted, xcodes will prompt interactively.
- **appleIdPassword**: *(string, optional)* Apple ID password. Required alongside `appleId` for non-interactive installs.

## Example usage

```json title="codify.jsonc"
[
{
"type": "xcodes",
"xcodeVersions": ["15.4"],
"selected": "15.4",
"os": ["macOS"]
}
]
```

Multiple versions side by side:

```json title="codify.jsonc"
[
{
"type": "xcodes",
"xcodeVersions": ["14.3.1", "15.4"],
"selected": "15.4",
"os": ["macOS"]
}
]
```

## Authentication

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.

For CI or fully non-interactive environments, add `appleId` and `appSpecificPassword` to the resource config:

```json title="codify.jsonc"
[
{
"type": "xcodes",
"xcodeVersions": ["15.4"],
"selected": "15.4",
"appleId": "your@apple.id",
"appleIdPassword": "<Replace me here!>",
"os": ["macOS"]
}
]
```

Note that 2FA will still trigger interactively even with credentials set — xcodes does not support fully headless 2FA bypass.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "default",
"version": "1.13.0",
"version": "1.14.0",
"description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux",
"main": "dist/index.js",
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { CursorResource } from './resources/cursor/cursor.js';
import { VscodeResource } from './resources/vscode/vscode.js';
import { WebStormResource } from './resources/webstorm/webstorm.js';
import { XcodeToolsResource } from './resources/xcode-tools/xcode-tools.js';
import { XcodesResource } from './resources/xcodes/xcodes-resource.js';
import { YumResource } from './resources/yum/yum.js';
import { PyCharmResource } from './resources/jetbrains/pycharm/pycharm.js';
import { ClionResource } from './resources/jetbrains/clion/clion.js';
Expand All @@ -86,6 +87,7 @@ runPlugin(Plugin.create(
[
new GitResource(),
new XcodeToolsResource(),
new XcodesResource(),
new PathResource(),
new AliasResource(),
new AliasesResource(),
Expand Down
23 changes: 23 additions & 0 deletions src/resources/xcodes/completions/xcodes.xcodeVersions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const XCODE_RELEASES_URL = 'https://xcodereleases.com/data.json';

interface XcodeRelease {
version: {
number: string;
release: { release?: boolean; beta?: number; rc?: number; dp?: number };
};
}

function toXcodesVersionString(release: XcodeRelease): string {
const { number, release: rel } = release.version;
if (rel.release) return number;
if (rel.beta != null) return `${number} Beta ${rel.beta}`;
if (rel.rc != null) return `${number} RC ${rel.rc}`;
if (rel.dp != null) return `${number} DP ${rel.dp}`;
return number;
}

export default async function loadXcodeVersions(): Promise<string[]> {
const response = await fetch(XCODE_RELEASES_URL);
const releases = await response.json() as XcodeRelease[];
return releases.map(toXcodesVersionString);
}
43 changes: 43 additions & 0 deletions src/resources/xcodes/selected-parameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getPty, ParameterSetting, SpawnStatus, StatefulParameter } from '@codifycli/plugin-core';

import { XcodesConfig } from './xcodes-resource.js';

export class XcodesSelectedParameter extends StatefulParameter<XcodesConfig, string> {
getSettings(): ParameterSetting {
return {
type: 'version',
};
}

override async refresh(): Promise<string | null> {
const $ = getPty();
const { data, status } = await $.spawnSafe('xcodes installed');
if (status === SpawnStatus.ERROR) return null;
return parseSelectedVersion(data);
}

override async add(version: string): Promise<void> {
const $ = getPty();
await $.spawn(`xcodes select "${version}"`, { interactive: true, stdin: true });
}

override async modify(newVersion: string): Promise<void> {
const $ = getPty();
await $.spawn(`xcodes select "${newVersion}"`, { interactive: true, stdin: true });
}

override async remove(): Promise<void> {
const $ = getPty();
await $.spawn('xcode-select --reset', { requiresRoot: true });
}
}

function parseSelectedVersion(output: string): string | null {
for (const line of output.split('\n')) {
if (line.includes('Selected')) {
const match = line.trim().match(/^(.+?)\s+\([^)]+\)/);
return match ? match[1].trim() : null;
}
}
return null;
}
43 changes: 43 additions & 0 deletions src/resources/xcodes/xcode-versions-parameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ArrayStatefulParameter, Plan, getPty } from '@codifycli/plugin-core';

import { XcodesConfig } from './xcodes-resource.js';

export class XcodeVersionsParameter extends ArrayStatefulParameter<XcodesConfig, string> {
override async refresh(_desired: string[] | null): Promise<string[] | null> {
const $ = getPty();
const { data } = await $.spawnSafe('xcodes installed');
return parseInstalledVersions(data);
}

override async addItem(version: string, plan: Plan<XcodesConfig>): Promise<void> {
const $ = getPty();
const { appleId, appleIdPassword } = plan.desiredConfig ?? {};

const env: Record<string, string> = {};
if (appleId) env['XCODES_USERNAME'] = appleId;
if (appleIdPassword) env['XCODES_PASSWORD'] = appleIdPassword;

await $.spawn(`xcodes install "${version}"`, {
interactive: true,
stdin: true,
...(Object.keys(env).length > 0 ? { env } : {}),
});
}

override async removeItem(version: string): Promise<void> {
const $ = getPty();
await $.spawn(`xcodes uninstall "${version}"`, { interactive: true });
}
}

function parseInstalledVersions(output: string): string[] {
return output
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const match = line.match(/^(.+?)\s+\([^)]+\)/);
return match ? match[1].trim() : null;
})
.filter((v): v is string => v !== null);
}
Loading
Loading